Skip to content

Commit c6c7c5a

Browse files
authored
Merge pull request #41 from OpenIPC/feature/agent-flash-cmd
Add `defib agent flash` one-command firmware install
2 parents 9526dc2 + 7f6bd7a commit c6c7c5a

File tree

2 files changed

+246
-6
lines changed

2 files changed

+246
-6
lines changed

README.md

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,20 +105,40 @@ Tested on real hardware with CRS112-8P-4S:
105105
| IVGHP203Y-AF | hi3516cv300 | `/dev/uart-IVGHP203Y-AF` |
106106
| IVG85HG50PYA-S | hi3516ev300 | `/dev/uart-IVG85HG50PYA-S` |
107107

108-
## Flash Agent (High-Speed Flash Dump)
108+
## Flash Agent (High-Speed Recovery)
109109

110110
Defib includes a bare-metal flash agent that runs directly on the SoC,
111111
replacing U-Boot in the boot chain. It communicates over a COBS binary
112-
protocol at 921600 baud — ~5x faster than U-Boot's `md.b` hex dump.
112+
protocol at 921600 baud for high-speed flash operations.
113+
114+
### One-Command Firmware Install
115+
116+
Flash a complete firmware image via UART in a single command:
117+
118+
```bash
119+
defib agent flash -c hi3516ev300 -i firmware.bin -p /dev/ttyUSB0
120+
```
121+
122+
Power-cycle the camera when prompted. The command handles everything:
123+
1. Uploads the bare-metal agent via boot protocol
124+
2. Switches to 921600 baud for high-speed transfer
125+
3. Streams firmware directly to flash (skips 0xFF sectors)
126+
4. Verifies CRC32 of the written data
127+
5. Reboots the device
128+
129+
Typical 8MB OpenIPC firmware on 16MB flash: **~2 minutes** total (upload +
130+
flash + verify + boot). No network required — just a USB-serial adapter.
131+
132+
### Other Agent Commands
113133

114134
```bash
115-
# 1. Upload the agent (power-cycle the camera when prompted)
135+
# Upload agent only (for manual operations)
116136
defib agent upload -c hi3516ev300 -p /dev/ttyUSB0
117137

118-
# 2. Dump the entire flash (address and size auto-detected)
138+
# Dump the entire flash (address and size auto-detected)
119139
defib agent read -p /dev/ttyUSB0 -o flash_dump.bin
120140

121-
# Query device info (flash size, RAM base, JEDEC ID)
141+
# Query device info (flash size, RAM base, JEDEC ID, agent version)
122142
defib agent info -p /dev/ttyUSB0
123143

124144
# Write data back to flash
@@ -130,7 +150,7 @@ defib agent scan -p /dev/ttyUSB0
130150

131151
Address defaults to flash base (`0x14000000`) and size is auto-detected
132152
from the device. Override with `-a` and `-s` if needed. Use `--no-verify`
133-
to skip the CRC32 check, or `--output-mode json` for automation. See
153+
to skip the CRC32 check, or `--output json` for automation. See
134154
[agent/README.md](agent/README.md) for protocol details and supported chips.
135155

136156
## Testing with QEMU

src/defib/cli/app.py

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -889,6 +889,226 @@ def on_progress(e: ProgressEvent) -> None:
889889
await transport.close()
890890

891891

892+
@agent_app.command("flash")
893+
def agent_flash(
894+
chip: str = typer.Option(..., "-c", "--chip", help="Chip model name"),
895+
input_file: str = typer.Option(..., "-i", "--input", help="Firmware binary file"),
896+
port: str = typer.Option("/dev/ttyUSB0", "-p", "--port", help="Serial port"),
897+
verify: bool = typer.Option(True, "--verify/--no-verify", help="CRC32 verify after write"),
898+
reboot: bool = typer.Option(True, "--reboot/--no-reboot", help="Reboot after flash"),
899+
output: str = typer.Option("human", "--output", help="Output mode: human, json"),
900+
) -> None:
901+
"""Flash firmware in one step: upload agent, write flash, verify, reboot.
902+
903+
Power-cycle the camera when prompted. The command handles everything:
904+
boot protocol upload, high-speed UART, streaming flash write with 0xFF
905+
sector skip, CRC32 verification, and device reboot.
906+
"""
907+
import asyncio
908+
asyncio.run(_agent_flash_async(chip, input_file, port, verify, reboot, output))
909+
910+
911+
async def _agent_flash_async(
912+
chip: str, input_file: str, port: str,
913+
verify: bool, reboot_device: bool, output: str,
914+
) -> None:
915+
import json as json_mod
916+
import time
917+
import zlib
918+
from pathlib import Path
919+
920+
from rich.console import Console
921+
922+
from defib.agent.client import FlashAgentClient, get_agent_binary
923+
from defib.firmware import get_cached_path
924+
from defib.profiles.loader import load_profile
925+
from defib.protocol.hisilicon_standard import HiSiliconStandard
926+
from defib.recovery.events import ProgressEvent
927+
from defib.transport.serial import SerialTransport
928+
929+
console = Console()
930+
FLASH_MEM = 0x14000000
931+
932+
# --- Load firmware file ---
933+
fw_path = Path(input_file)
934+
if not fw_path.exists():
935+
msg = f"Firmware file not found: {input_file}"
936+
if output == "json":
937+
print(json_mod.dumps({"event": "error", "message": msg}))
938+
else:
939+
console.print(f"[red]{msg}[/red]")
940+
raise typer.Exit(1)
941+
942+
firmware = fw_path.read_bytes()
943+
fw_crc = zlib.crc32(firmware) & 0xFFFFFFFF
944+
945+
# --- Find agent binary ---
946+
agent_path = get_agent_binary(chip)
947+
if not agent_path:
948+
msg = f"No agent binary for '{chip}'"
949+
if output == "json":
950+
print(json_mod.dumps({"event": "error", "message": msg}))
951+
else:
952+
console.print(f"[red]{msg}[/red]")
953+
raise typer.Exit(1)
954+
955+
agent_data = agent_path.read_bytes()
956+
957+
# --- Get SPL from cached U-Boot ---
958+
profile = load_profile(chip)
959+
cached_fw = get_cached_path(chip)
960+
if not cached_fw:
961+
from defib.firmware import download_firmware
962+
if output == "human":
963+
console.print("Downloading U-Boot for SPL...")
964+
cached_fw = download_firmware(chip)
965+
spl_data = cached_fw.read_bytes()[:profile.spl_max_size]
966+
967+
if output == "human":
968+
console.print(f"Firmware: [cyan]{fw_path.name}[/cyan] ({len(firmware)} bytes, CRC {fw_crc:#010x})")
969+
console.print(f"Agent: [cyan]{agent_path.name}[/cyan] ({len(agent_data)} bytes)")
970+
console.print("\n[yellow]Power-cycle the camera now![/yellow]\n")
971+
972+
# --- Phase 1: Upload agent via boot protocol ---
973+
transport = await SerialTransport.create(port)
974+
protocol = HiSiliconStandard()
975+
protocol.set_profile(profile)
976+
977+
def on_boot_progress(e: ProgressEvent) -> None:
978+
if e.message:
979+
if output == "human":
980+
console.print(f" {e.message}")
981+
elif output == "json":
982+
print(json_mod.dumps({"event": "boot", "message": e.message}), flush=True)
983+
984+
hs = await protocol.handshake(transport, on_boot_progress)
985+
if not hs.success:
986+
msg = "Handshake failed"
987+
if output == "json":
988+
print(json_mod.dumps({"event": "error", "message": msg}))
989+
else:
990+
console.print(f"[red]{msg}[/red]")
991+
await transport.close()
992+
raise typer.Exit(1)
993+
994+
result = await protocol.send_firmware(
995+
transport, agent_data, on_boot_progress, spl_override=spl_data,
996+
payload_label="Agent",
997+
)
998+
if not result.success:
999+
msg = result.error or "Upload failed"
1000+
if output == "json":
1001+
print(json_mod.dumps({"event": "error", "message": msg}))
1002+
else:
1003+
console.print(f"[red]Upload failed:[/red] {msg}")
1004+
await transport.close()
1005+
raise typer.Exit(1)
1006+
1007+
# --- Phase 2: Connect to agent ---
1008+
import asyncio as aio
1009+
await transport.close()
1010+
await aio.sleep(2)
1011+
transport = await SerialTransport.create(port)
1012+
1013+
client = FlashAgentClient(transport, chip)
1014+
if not await client.connect(timeout=10.0):
1015+
msg = "Agent not responding"
1016+
if output == "json":
1017+
print(json_mod.dumps({"event": "error", "message": msg}))
1018+
else:
1019+
console.print(f"[red]{msg}[/red]")
1020+
await transport.close()
1021+
raise typer.Exit(1)
1022+
1023+
info = await client.get_info()
1024+
flash_size = int(info.get("flash_size", 0))
1025+
1026+
if output == "human":
1027+
console.print(f"[green]Agent ready![/green] Flash: {flash_size // 1024}KB")
1028+
1029+
if flash_size > 0 and len(firmware) > flash_size:
1030+
msg = f"Firmware ({len(firmware)} bytes) exceeds flash size ({flash_size} bytes)"
1031+
if output == "json":
1032+
print(json_mod.dumps({"event": "error", "message": msg}))
1033+
else:
1034+
console.print(f"[red]{msg}[/red]")
1035+
await transport.close()
1036+
raise typer.Exit(1)
1037+
1038+
# --- Phase 3: Flash firmware ---
1039+
if output == "human":
1040+
console.print(f"Flashing {len(firmware)} bytes...")
1041+
1042+
t0 = time.monotonic()
1043+
last_pct = [0]
1044+
1045+
def on_flash_progress(done: int, total: int) -> None:
1046+
pct = done * 100 // total if total > 0 else 0
1047+
if pct >= last_pct[0] + 5:
1048+
elapsed = time.monotonic() - t0
1049+
speed = done / elapsed if elapsed > 0 else 0
1050+
if output == "human":
1051+
print(f"\r {pct}% ({done // 1024}KB / {total // 1024}KB) "
1052+
f"{speed:.0f} B/s", end="", flush=True)
1053+
elif output == "json":
1054+
print(json_mod.dumps({"event": "flash", "pct": pct, "speed": int(speed)}),
1055+
flush=True)
1056+
last_pct[0] = pct
1057+
1058+
ok = await client.write_flash(0, firmware, on_progress=on_flash_progress)
1059+
elapsed = time.monotonic() - t0
1060+
1061+
if output == "human":
1062+
print() # newline after progress
1063+
if not ok:
1064+
msg = f"Flash write failed after {elapsed:.1f}s"
1065+
if output == "json":
1066+
print(json_mod.dumps({"event": "error", "message": msg}))
1067+
else:
1068+
console.print(f"[red]{msg}[/red]")
1069+
await transport.close()
1070+
raise typer.Exit(1)
1071+
1072+
speed = len(firmware) / elapsed if elapsed > 0 else 0
1073+
if output == "human":
1074+
console.print(f" Written in {elapsed:.1f}s ({speed:.0f} B/s)")
1075+
1076+
# --- Phase 4: Verify ---
1077+
if verify:
1078+
if output == "human":
1079+
console.print(" Verifying CRC32...")
1080+
dev_crc = await client.crc32(FLASH_MEM, len(firmware))
1081+
match = dev_crc == fw_crc
1082+
if output == "human":
1083+
if match:
1084+
console.print(f" CRC32: [green]OK[/green] ({fw_crc:#010x})")
1085+
else:
1086+
console.print(f" CRC32: [red]MISMATCH[/red] (device {dev_crc:#010x} != {fw_crc:#010x})")
1087+
if not match:
1088+
await transport.close()
1089+
raise typer.Exit(1)
1090+
1091+
# --- Phase 5: Reboot ---
1092+
if reboot_device:
1093+
if output == "human":
1094+
console.print(" Rebooting...")
1095+
await client.reboot()
1096+
1097+
if output == "human":
1098+
console.print(f"\n[green bold]Done![/green bold] Firmware flashed in {elapsed:.0f}s")
1099+
elif output == "json":
1100+
print(json_mod.dumps({
1101+
"event": "done",
1102+
"bytes": len(firmware),
1103+
"elapsed": round(elapsed, 1),
1104+
"speed": int(speed),
1105+
"verified": verify,
1106+
"rebooted": reboot_device,
1107+
}))
1108+
1109+
await transport.close()
1110+
1111+
8921112
@agent_app.command("info")
8931113
def agent_info(
8941114
port: str = typer.Option("/dev/ttyUSB0", "-p", "--port", help="Serial port"),

0 commit comments

Comments
 (0)