@@ -27,7 +27,8 @@ use zip::write::SimpleFileOptions;
2727use zip:: ZipWriter ;
2828
2929use crate :: {
30- cmd_run, cmd_usb_diag, config, debug_packets, journal, pico_cache, pico_state, protocol,
30+ cmd_persona, cmd_run, cmd_usb_diag, config, debug_packets, journal, pico_cache, pico_mode,
31+ pico_state, protocol,
3132} ;
3233
3334use collect:: { bundle_log_prefix, collect_crash_file_names, collect_setup_transcript_names} ;
@@ -50,6 +51,9 @@ use usb_packet_summary::{
5051} ;
5152
5253const BUNDLE_DEBUG_PACKET_HARVEST_TIMEOUT : Duration = Duration :: from_secs ( 2 ) ;
54+ const BUNDLE_DEBUG_PERSONA_WAIT : Duration = Duration :: from_secs ( 60 ) ;
55+ const BUNDLE_DEBUG_USB_SETTLE : Duration = Duration :: from_secs ( 3 ) ;
56+ const BUNDLE_RESTORE_PERSONA_WAIT : Duration = Duration :: from_secs ( 60 ) ;
5357
5458/// Structured result of a bundle build. Returned by `build_bundle` so
5559/// callers get a typed answer without
@@ -107,6 +111,12 @@ struct RetainedDebugPacketLog {
107111 text : String ,
108112}
109113
114+ #[ derive( Clone , Debug ) ]
115+ struct BundleUsbPacketCapture {
116+ text : String ,
117+ debug_target : Option < cmd_run:: PicoTarget > ,
118+ }
119+
110120#[ derive( Default ) ]
111121struct CaptureLog {
112122 lines : Vec < String > ,
@@ -763,7 +773,7 @@ async fn capture_usb_diag_text() -> UsbDiagBundle {
763773 Suggested next step:\n \
764774 - Make sure the Pico is powered, has joined Wi-Fi, and is still plugged into the console adapter.\n \
765775 - Run `couchlink.exe bundle` again immediately after the failure.\n \
766- - If the Pico is on Wi-Fi but broadcast discovery is blocked, run `couchlink.exe doctor` once so the last-known IP is saved .\n \n \
776+ - If the Pico is on Wi-Fi but broadcast discovery is blocked, choose `Enter Pico IP manually` from the guided menu once, then run bundle again .\n \n \
767777 Diagnostic details:\n \
768778 error={e:#}\n "
769779 ) ,
@@ -1065,8 +1075,15 @@ async fn capture_one_pico(
10651075 pico_state_status = target_pico_state_status;
10661076 pico_diag_status = target_pico_diag_status;
10671077 pico_diag_text = target_pico_diag_text;
1068- usb_packets_text =
1078+ let packet_capture =
10691079 bundle_usb_packets_for_target ( & seed. uid , target, & pico_diag_text, capture_log) . await ;
1080+ if let Some ( debug_target) = packet_capture. debug_target . as_ref ( ) {
1081+ snapshot =
1082+ pico_cache:: PicoStateSnapshot :: from_target ( "bundle-debug-capture" , debug_target)
1083+ . with_outcome ( "bundle: automatic debug input capture" ) ;
1084+ pico_cache:: record ( snapshot. clone ( ) ) ;
1085+ }
1086+ usb_packets_text = packet_capture. text ;
10701087 usb_diag_status = target_usb_diag_status;
10711088 usb_diag_text = target_usb_diag_text;
10721089 state_json_from_snapshot ( & snapshot)
@@ -1154,11 +1171,153 @@ async fn bundle_usb_packets_for_target(
11541171 target : & cmd_run:: PicoTarget ,
11551172 fallback_diag_text : & str ,
11561173 capture_log : & mut CaptureLog ,
1157- ) -> String {
1158- if target. persona != protocol:: Persona :: Debug {
1159- return usb_packets_text_from_diag ( uid, fallback_diag_text) ;
1174+ ) -> BundleUsbPacketCapture {
1175+ if target. persona == protocol:: Persona :: Debug {
1176+ return BundleUsbPacketCapture {
1177+ text : harvest_debug_packets_for_target ( uid, target, fallback_diag_text, capture_log)
1178+ . await ,
1179+ debug_target : Some ( target. clone ( ) ) ,
1180+ } ;
1181+ }
1182+
1183+ let original_persona = target. persona ;
1184+ let started = Instant :: now ( ) ;
1185+ let mut note = String :: new ( ) ;
1186+ match pico_mode:: request_set_persona ( target, protocol:: Persona :: Debug ) . await {
1187+ Ok ( ( ) ) => {
1188+ capture_log. record (
1189+ format ! ( "per_pico.{uid}.debug_cycle_request" ) ,
1190+ started,
1191+ "sent" ,
1192+ 1 ,
1193+ format ! ( "from={} to=debug" , original_persona. label( ) ) ,
1194+ ) ;
1195+ }
1196+ Err ( e) => {
1197+ capture_log. record (
1198+ format ! ( "per_pico.{uid}.debug_cycle_request" ) ,
1199+ started,
1200+ "error" ,
1201+ 0 ,
1202+ format ! ( "{e:#}" ) ,
1203+ ) ;
1204+ let mut text = usb_packets_text_from_diag ( uid, fallback_diag_text) ;
1205+ let _ = writeln ! (
1206+ text,
1207+ "# bundle-debug-cycle status=request_failed from={} error={}" ,
1208+ original_persona. label( ) ,
1209+ sanitize_log_field( & format!( "{e:#}" ) )
1210+ ) ;
1211+ return BundleUsbPacketCapture {
1212+ text,
1213+ debug_target : None ,
1214+ } ;
1215+ }
1216+ }
1217+
1218+ let started = Instant :: now ( ) ;
1219+ let matched = match cmd_persona:: wait_for_persona (
1220+ & [ target. info . unique_id_short ] ,
1221+ protocol:: Persona :: Debug ,
1222+ BUNDLE_DEBUG_PERSONA_WAIT ,
1223+ )
1224+ . await
1225+ {
1226+ Ok ( matched) => matched,
1227+ Err ( e) => {
1228+ capture_log. record (
1229+ format ! ( "per_pico.{uid}.debug_cycle_wait" ) ,
1230+ started,
1231+ "error" ,
1232+ 0 ,
1233+ format ! ( "{e:#}" ) ,
1234+ ) ;
1235+ let mut text = usb_packets_text_from_diag ( uid, fallback_diag_text) ;
1236+ let _ = writeln ! (
1237+ text,
1238+ "# bundle-debug-cycle status=wait_failed from={} error={}" ,
1239+ original_persona. label( ) ,
1240+ sanitize_log_field( & format!( "{e:#}" ) )
1241+ ) ;
1242+ return BundleUsbPacketCapture {
1243+ text,
1244+ debug_target : None ,
1245+ } ;
1246+ }
1247+ } ;
1248+ let observed = format_observed_personas ( & matched) ;
1249+ let debug_target = matched
1250+ . iter ( )
1251+ . find ( |pico| pico. persona == protocol:: Persona :: Debug )
1252+ . cloned ( ) ;
1253+ capture_log. record (
1254+ format ! ( "per_pico.{uid}.debug_cycle_wait" ) ,
1255+ started,
1256+ if debug_target. is_some ( ) {
1257+ "confirmed"
1258+ } else {
1259+ "not_confirmed"
1260+ } ,
1261+ matched. len ( ) ,
1262+ format ! ( "observed={observed}" ) ,
1263+ ) ;
1264+
1265+ let Some ( debug_target) = debug_target else {
1266+ if let Some ( current) = matched. first ( ) {
1267+ if current. persona != original_persona {
1268+ restore_persona_after_bundle ( uid, current, original_persona, capture_log) . await ;
1269+ }
1270+ }
1271+ let mut text = usb_packets_text_from_diag ( uid, fallback_diag_text) ;
1272+ let _ = writeln ! (
1273+ text,
1274+ "# bundle-debug-cycle status=not_confirmed from={} observed={}" ,
1275+ original_persona. label( ) ,
1276+ observed
1277+ ) ;
1278+ return BundleUsbPacketCapture {
1279+ text,
1280+ debug_target : None ,
1281+ } ;
1282+ } ;
1283+
1284+ capture_log. record_duration (
1285+ format ! ( "per_pico.{uid}.debug_usb_settle" ) ,
1286+ BUNDLE_DEBUG_USB_SETTLE . as_millis ( ) as u64 ,
1287+ "sleep" ,
1288+ 0 ,
1289+ "allow USB host to enumerate and poll debug input persona" ,
1290+ ) ;
1291+ tokio:: time:: sleep ( BUNDLE_DEBUG_USB_SETTLE ) . await ;
1292+
1293+ let text =
1294+ harvest_debug_packets_for_target ( uid, & debug_target, fallback_diag_text, capture_log) . await ;
1295+
1296+ if original_persona != protocol:: Persona :: Debug {
1297+ restore_persona_after_bundle ( uid, & debug_target, original_persona, capture_log) . await ;
1298+ let _ = writeln ! (
1299+ note,
1300+ "# bundle-debug-cycle restore_requested persona={}" ,
1301+ original_persona. label( )
1302+ ) ;
11601303 }
11611304
1305+ let mut text = text;
1306+ if !note. is_empty ( ) {
1307+ text. push_str ( & note) ;
1308+ }
1309+ BundleUsbPacketCapture {
1310+ text,
1311+ debug_target : Some ( debug_target) ,
1312+ }
1313+ }
1314+
1315+ async fn harvest_debug_packets_for_target (
1316+ uid : & str ,
1317+ target : & cmd_run:: PicoTarget ,
1318+ fallback_diag_text : & str ,
1319+ capture_log : & mut CaptureLog ,
1320+ ) -> String {
11621321 let started = Instant :: now ( ) ;
11631322 match debug_packets:: capture_run_diag_log ( target. peer , BUNDLE_DEBUG_PACKET_HARVEST_TIMEOUT )
11641323 . await
@@ -1201,6 +1360,82 @@ async fn bundle_usb_packets_for_target(
12011360 }
12021361}
12031362
1363+ async fn restore_persona_after_bundle (
1364+ uid : & str ,
1365+ target : & cmd_run:: PicoTarget ,
1366+ persona : protocol:: Persona ,
1367+ capture_log : & mut CaptureLog ,
1368+ ) {
1369+ let started = Instant :: now ( ) ;
1370+ match pico_mode:: request_set_persona ( target, persona) . await {
1371+ Ok ( ( ) ) => capture_log. record (
1372+ format ! ( "per_pico.{uid}.restore_persona_request" ) ,
1373+ started,
1374+ "sent" ,
1375+ 1 ,
1376+ format ! ( "persona={}" , persona. label( ) ) ,
1377+ ) ,
1378+ Err ( e) => {
1379+ capture_log. record (
1380+ format ! ( "per_pico.{uid}.restore_persona_request" ) ,
1381+ started,
1382+ "error" ,
1383+ 0 ,
1384+ format ! ( "{e:#}" ) ,
1385+ ) ;
1386+ return ;
1387+ }
1388+ }
1389+
1390+ let started = Instant :: now ( ) ;
1391+ match cmd_persona:: wait_for_persona (
1392+ & [ target. info . unique_id_short ] ,
1393+ persona,
1394+ BUNDLE_RESTORE_PERSONA_WAIT ,
1395+ )
1396+ . await
1397+ {
1398+ Ok ( matched) => {
1399+ let restored = matched. iter ( ) . find ( |pico| pico. persona == persona) ;
1400+ capture_log. record (
1401+ format ! ( "per_pico.{uid}.restore_persona_wait" ) ,
1402+ started,
1403+ if restored. is_some ( ) {
1404+ "confirmed"
1405+ } else {
1406+ "not_confirmed"
1407+ } ,
1408+ matched. len ( ) ,
1409+ format ! ( "observed={}" , format_observed_personas( & matched) ) ,
1410+ ) ;
1411+ if let Some ( restored) = restored {
1412+ pico_cache:: record (
1413+ pico_cache:: PicoStateSnapshot :: from_target ( "bundle-restore" , restored)
1414+ . with_outcome ( format ! ( "restored_{}" , persona. label( ) ) ) ,
1415+ ) ;
1416+ }
1417+ }
1418+ Err ( e) => capture_log. record (
1419+ format ! ( "per_pico.{uid}.restore_persona_wait" ) ,
1420+ started,
1421+ "error" ,
1422+ 0 ,
1423+ format ! ( "{e:#}" ) ,
1424+ ) ,
1425+ }
1426+ }
1427+
1428+ fn format_observed_personas ( targets : & [ cmd_run:: PicoTarget ] ) -> String {
1429+ if targets. is_empty ( ) {
1430+ return "none" . to_string ( ) ;
1431+ }
1432+ targets
1433+ . iter ( )
1434+ . map ( |target| format ! ( "{}:{}" , target. uid_hex( ) , target. persona. label( ) ) )
1435+ . collect :: < Vec < _ > > ( )
1436+ . join ( "," )
1437+ }
1438+
12041439fn saved_picos_from_config ( cfg : & config:: Config ) -> Vec < config:: PicoIdentity > {
12051440 let mut out = Vec :: new ( ) ;
12061441 let mut seen = BTreeSet :: new ( ) ;
@@ -1664,13 +1899,13 @@ fn debug_capture_verdict_text(
16641899 out. push_str ( "next_steps=\n " ) ;
16651900 if summary. aggregate . packet_lines == 0 {
16661901 out. push_str (
1667- "- Switch the target Pico to debug input mode, reproduce adapter traffic, then run bundle again .\n " ,
1902+ "- Run bundle again with the Pico powered and connected to the adapter; bundle switches to debug input mode and restores the original mode automatically .\n " ,
16681903 ) ;
16691904 out. push_str (
1670- "- Keep the bridge stream running while reproducing the issue so retained debug packet harvests can write debug-packets/*.log .\n " ,
1905+ "- If bundle-capture.txt shows debug_cycle_request, debug_cycle_wait, debug_packet_harvest, or GET_LOG failures, use that row's reason as the failure point .\n " ,
16711906 ) ;
16721907 out. push_str (
1673- "- If harvest_statuses shows error, check bundle-capture.txt and debug-packets/*.log for GET_LOG failures and timing .\n " ,
1908+ "- If retained debug-packets/*.log files are present, include the whole bundle; those files contain prior stream-time harvest evidence .\n " ,
16741909 ) ;
16751910 } else {
16761911 out. push_str (
0 commit comments