@@ -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" )
8931113def agent_info (
8941114 port : str = typer .Option ("/dev/ttyUSB0" , "-p" , "--port" , help = "Serial port" ),
0 commit comments