Skip to content

rockusb: read flash over USB, and reset a stale usbplug to MaskROM - #134

Merged
widgetii merged 3 commits into
masterfrom
feat/rockusb-dump-flash
Sep 8, 2026
Merged

rockusb: read flash over USB, and reset a stale usbplug to MaskROM#134
widgetii merged 3 commits into
masterfrom
feat/rockusb-dump-flash

Conversation

@widgetii

@widgetii widgetii commented Sep 7, 2026

Copy link
Copy Markdown
Member

What

Adds defib dump-flash reading over the RockUSB path, so a board whose boot ROM
only answers on USB (e.g. RV1106) can be backed up — the one thing missing
before any USB write can be attempted safely.

defib dump-flash -c rv1106 --ddr ... --usbplug ... \
                 --partition rootfs -o rootfs.bin
  • --chip is optional; the serial path is untouched for chips that already
    worked. For a USB-only profile, -p isn't consulted.
  • Capacity comes from READ_FLASH_INFO, not a guess or flag. A reply claiming
    zero capacity is refused (loader up but flash not brought up → a file full of
    nothing), and a range past the reported end is refused (the LBA commands just
    stall there).
  • --partition reads one region from the profile; the usbplug's LBA space maps
    directly onto the kernel's mtd partitions, so it compares byte-for-byte with
    cat /dev/mtdN on a booted board.
  • Reads stream to disk rather than buffering in memory.

Evidence

Exercised on an RV1106 (Luckfox Pico Max, 256 MB SPI-NAND) this session:
READ_FLASH_INFO reported 255.5 MiB (523264 sectors), block 128 KiB, page 2048 B — the 0.5 MiB gap being the FTL reserve, and the block/page matching
/proc/mtd on the same board booted. Partition reads matched byte-for-byte.

Tests: tests/test_rockusb_flashinfo.py (flash-info parse, zero-capacity and
out-of-range refusal). Full suite 755 passed, 2 skipped.

Notes

Rebased onto current master (post #133). Depends on nothing outside it. A
separate return-to-MaskROM recovery enhancement (handling a stale usbplug a
prior run left running) is still WIP on my side and not included here.

The one command that had no -c/--chip, which is also the one thing
missing before any write can be attempted safely: there was no way to
back up a board whose boot ROM only answers on USB.

    defib dump-flash -c rv1106 --ddr ... --usbplug ... \
                     --partition rootfs -o rootfs.bin

--chip is optional, so the serial path is untouched for every chip that
already worked. Given one whose profile says USB, -p is not consulted.

Capacity comes from READ_FLASH_INFO rather than a guess or a flag. On an
RV1106 that reports 255.5 MiB for a 256 MiB part — the difference being
the translation layer's own reserve — along with a 128 KiB erase block
and 2 KiB page, which match what the kernel puts in /proc/mtd on the
same board. A reply claiming zero capacity is refused: the loader is
then running but has not brought flash up, and dumping from it would
produce a convincing file full of nothing. A range running past the
reported end is refused too, since the LBA commands simply stop
answering there and a dump ending in a stall is worse than one that
declines to start.

--partition names a region from the profile instead of pulling the whole
device, which for a 255 MiB part is usually what you want. The usbplug's
LBA space maps directly onto the kernel's mtd partitions, so a partition
read this way compares byte for byte with `cat /dev/mtdN` on a booted
board.

Reads stream to disk rather than accumulating in memory.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Add RockUSB flash dumping for USB-only boards

✨ Enhancement 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Add RockUSB flash dumping while preserving existing serial behavior.
• Stream full-device or named-partition reads using device-reported flash capacity.
• Reject invalid metadata and out-of-range reads before creating misleading dumps.
Diagram

sequenceDiagram
    actor User
    participant CLI as Dump CLI
    participant Profile as Chip Profile
    participant Recovery as RockUSB Recovery
    participant Flash as Flash FTL
    participant File as Output File
    User->>CLI: Start USB dump
    CLI->>Profile: Resolve mode and partition
    CLI->>Recovery: Load usbplug and open target
    Recovery->>Flash: READ_FLASH_INFO
    Flash-->>Recovery: Capacity and geometry
    CLI->>Recovery: Stream validated LBA range
    loop Chunked reads
        Recovery->>Flash: READ_LBA
        Flash-->>Recovery: Flash chunk
        Recovery->>File: Write chunk
    end
Loading
High-Level Assessment

The PR’s approach is appropriate: it reuses the existing RockUSB recovery abstraction, trusts device-reported FTL capacity, validates profile extents before reading, and streams chunks directly to disk. Manual capacity flags or whole-image buffering would be less reliable and unnecessarily memory-intensive.

Files changed (6) +330 / -5

Enhancement (5) +252 / -5
app.pyRoute USB-only flash dumps through RockUSB recovery +128/-5

Route USB-only flash dumps through RockUSB recovery

• Adds chip, partition, loader, USB selection, wait, and power-cycle options to dump-flash while leaving the serial path unchanged. The USB path resolves profile partitions, initializes the loader, validates ranges against reported capacity, streams data to disk, and emits human or JSON progress and completion output.

src/defib/cli/app.py

events.pyAdd flash-read progress stage +1/-0

Add flash-read progress stage

• Introduces the FLASH_READ recovery stage so streamed USB dumps can report distinct read progress events.

src/defib/recovery/events.py

__init__.pyExport RockUSB flash information APIs +6/-0

Export RockUSB flash information APIs

• Re-exports the flash-info reply length, data model, and parser through the RockUSB package interface.

src/defib/rockusb/init.py

protocol.pyParse and validate READ_FLASH_INFO replies +75/-0

Parse and validate READ_FLASH_INFO replies

• Adds an immutable FlashInfo model with capacity and geometry helpers. Parses the 11-byte RockUSB response and rejects short replies or zero-capacity devices before dumping begins.

src/defib/rockusb/protocol.py

recovery.pyStream flash reads through RockUSB +42/-0

Stream flash reads through RockUSB

• Adds device flash-information queries and chunked READ_LBA dumping to a caller-provided sink. Each chunk is written immediately and emits FLASH_READ progress, avoiding full-image memory buffering.

src/defib/rockusb/recovery.py

Tests (1) +78 / -0
test_rockusb_flashinfo.pyTest flash metadata parsing and capacity safety +78/-0

Test flash metadata parsing and capacity safety

• Validates a captured RV1106 flash-info reply, derived capacity and geometry, readable formatting, malformed and zero-capacity rejection, and trailing-byte handling. It also verifies every RV1106 profile partition fits within the reported device capacity.

tests/test_rockusb_flashinfo.py

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 7, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. A failed dump destroys the prior backup ✓ Resolved 🐞 Bug ☼ Reliability
Description
_dump_flash_usb_async opens the requested destination with wb and streams directly into it,
while its failure handler neither restores the original file nor removes the incomplete replacement.
If an LBA read, filesystem write, or output callback fails after opening the file, any prior backup
at that path is lost and a truncated dump remains there.
Code

src/defib/cli/app.py[R3933-3936]

+        with target.open("wb") as fh:
+            written = await recovery.dump_image(
+                start, sectors, fh.write,
+                on_progress=_usb_progress_printer(output),
Evidence
The final destination is opened in truncating mode before dump_image completes, and the exception
path only reports the error. dump_image writes each chunk immediately, so a failure after any
successful chunk leaves the destination partially populated.

src/defib/cli/app.py[3925-3940]
src/defib/rockusb/recovery.py[218-231]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
USB flash dumps write directly to the final destination. A failed transfer therefore truncates any existing backup and leaves a partial image at the requested path.
## Issue Context
Create a temporary file in the destination directory, stream the dump into it, and atomically replace the destination only after the entire transfer and file close succeed. Remove the temporary file on every failure path.
## Fix Focus Areas
- src/defib/cli/app.py[3925-3940]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can tweak Display preferences with a live preview to see your comment before it ships

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/defib/cli/app.py Outdated
A usbplug a previous process left running wedges on the next command from a
fresh process, so every tool in this space re-uploads the loader each run.
_open_usb_target now notices a device still in loader mode, sends it back to
MaskROM (RockchipRecovery.return_to_maskrom) and re-acquires it there before
uploading — so a re-run after an abandoned transfer recovers on its own instead
of failing on the first command. If the loader refuses to reset it says so and
points at a BOOT + replug.
@widgetii widgetii changed the title dump-flash: read flash over USB, not just a U-Boot console rockusb: read flash over USB, and reset a stale usbplug to MaskROM Sep 7, 2026
@widgetii

widgetii commented Sep 7, 2026

Copy link
Copy Markdown
Member Author

Folded the stale-usbplug recovery in as a second commit (rather than a separate PR): _open_usb_target now resets a running loader back to MaskROM before reuse, with RockchipRecovery.return_to_maskrom + tests. Full suite 758 passed / 2 skipped, ruff clean.

_dump_flash_usb_async opened the destination with "wb" and streamed straight
into it, so a read/write/callback failure part-way through truncated any
existing backup at that path and left a partial image in its place — the worst
outcome for a tool whose whole job is a safe backup. Stream to a temp file
beside the destination and atomically replace it only once the transfer and
close both succeed; remove the temp on every failure path.
@widgetii

widgetii commented Sep 7, 2026

Copy link
Copy Markdown
Member Author

Addressed the atomic-dump finding (commit bbd307a): dump-flash now streams to a <name>.partial file beside the destination and atomically replaces the target only after the transfer and close both succeed, removing the temp on any failure path. Added TestDumpAtomicity — a mid-dump failure keeps the prior backup and leaves no partial; a success replaces the target. Full suite 760 passed / 2 skipped, ruff clean.

@widgetii
widgetii merged commit a2bc857 into master Sep 8, 2026
13 checks passed
@widgetii
widgetii deleted the feat/rockusb-dump-flash branch September 8, 2026 06:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant