diff --git a/crates/fbuild-daemon/src/handlers/operations/deploy.rs b/crates/fbuild-daemon/src/handlers/operations/deploy.rs index 8cf16d49..4d0b17c4 100644 --- a/crates/fbuild-daemon/src/handlers/operations/deploy.rs +++ b/crates/fbuild-daemon/src/handlers/operations/deploy.rs @@ -1107,6 +1107,8 @@ pub async fn deploy( r.message, ), Ok(r) => { + let mut failure_stderr = Some(r.stderr); + append_warning_to_stderr(&mut failure_stderr, deploy_port_warning); return ( StatusCode::INTERNAL_SERVER_ERROR, Json(OperationResponse { @@ -1119,13 +1121,14 @@ pub async fn deploy( output_dir: reported_output_dir.clone(), launch_url: None, stdout: Some(r.stdout), - stderr: Some(r.stderr), + stderr: failure_stderr, }), ); } Err(e) => { let mut response = deploy_error_response(request_id, &e); response.usb_recovery = usb_recovery_request; + append_warning_to_stderr(&mut response.stderr, deploy_port_warning); return (StatusCode::INTERNAL_SERVER_ERROR, Json(response)); } }; diff --git a/crates/fbuild-daemon/src/handlers/operations/deploy_port.rs b/crates/fbuild-daemon/src/handlers/operations/deploy_port.rs index c7f06e01..5dc5c3f2 100644 --- a/crates/fbuild-daemon/src/handlers/operations/deploy_port.rs +++ b/crates/fbuild-daemon/src/handlers/operations/deploy_port.rs @@ -55,9 +55,27 @@ fn choose_deploy_port_with_profile_lookup( if platform == Platform::RaspberryPi { let expected_generation = rp_generation_for(board); let board_profile = rp_board_profile_id(board_id, board).and_then(profile_lookup); + let mismatched = if board_profile.is_some() { + rp_generation_mates( + &devices, + expected_generation, + fbuild_core::usb::profiles::profiles_for, + ) + } else { + Vec::new() + }; let (matches, unhealthy) = partition_rp_candidates_for_board(devices, board_profile.as_ref(), expected_generation); - return rp_deploy_choice(matches, unhealthy); + let no_exact_match = matches.is_empty(); + let mut choice = rp_deploy_choice(matches, unhealthy); + if no_exact_match && !mismatched.is_empty() { + let note = identity_mismatch_note(board_id, &mismatched); + choice.warning = Some(match choice.warning.take() { + Some(existing) => format!("{existing}; {note}"), + None => note, + }); + } + return choice; } let mut candidates: Vec<_> = devices @@ -115,6 +133,34 @@ fn choose_deploy_port_with_profile_lookup( } } +/// Healthy connected RP-generation CDC ports whose observed identity is not +/// the requested board's. They are never auto-selected (W and non-W stay +/// distinct), but naming them turns a bare "no port" into an actionable +/// diagnosis (FastLED/fbuild#1430). +fn rp_generation_mates( + devices: &[DeviceState], + expected: RpGeneration, + profiles_of: impl Fn(u16, u16) -> Vec, +) -> Vec { + devices + .iter() + .filter(|device| device.is_connected && !device.port_health.is_known_unhealthy()) + .filter_map(|device| { + let (vid, pid) = device.vid.zip(device.pid)?; + rp_profiles_match_generation(&profiles_of(vid, pid), expected) + .then(|| format!("{} ({vid:04x}:{pid:04x})", device.port)) + }) + .collect() +} + +fn identity_mismatch_note(board_id: Option<&str>, mates: &[String]) -> String { + format!( + "connected RP-series CDC port(s) {} are running firmware whose USB identity does not match {}'s runtime identity; a running image's identity reflects the last firmware flashed, not the board, so fbuild will not auto-select it — pass -p/--port to deploy anyway", + mates.join(", "), + board_id.unwrap_or("the target environment"), + ) +} + fn rp_board_profile_id<'a>( board_id: Option<&'a str>, board: Option<&'a BoardConfig>, @@ -563,6 +609,30 @@ mod tests { } } + #[test] + fn identity_mismatch_names_the_attached_port_and_the_remedy() { + let pico = runtime_profile(Some("raspberrypi"), Some("rp2350"), false); + let mates = rp_generation_mates( + &[ + device("/dev/ttyACM0", Some(0x2E8A), Some(0x000F)), + device("/dev/ttyACM2", Some(0x303A), Some(0x1001)), + ], + RpGeneration::Rp2350, + |vid, _| { + if vid == 0x2E8A { + vec![pico.clone()] + } else { + Vec::new() + } + }, + ); + assert_eq!(mates, vec!["/dev/ttyACM0 (2e8a:000f)".to_string()]); + let note = identity_mismatch_note(Some("rpipico2w"), &mates); + assert!(note.contains("/dev/ttyACM0"), "{note}"); + assert!(note.contains("rpipico2w"), "{note}"); + assert!(note.contains("-p/--port"), "{note}"); + } + #[test] fn raspberry_pi_runtime_selection_falls_back_when_profile_is_unavailable() { let pico = runtime_profile(Some("raspberrypi"), Some("rp2040"), false);