Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion crates/fbuild-daemon/src/handlers/operations/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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));
}
};
Expand Down
72 changes: 71 additions & 1 deletion crates/fbuild-daemon/src/handlers/operations/deploy_port.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<fbuild_core::usb::profiles::UsbTransportProfile>,
) -> Vec<String> {
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 <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>,
Expand Down Expand Up @@ -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);
Expand Down
Loading