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
2 changes: 1 addition & 1 deletion agents/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ routing table sends an agent here by task.
harness for `port_class` + `family_for_port` validation against an
actual ESP32. **Reach for this** when you touch
`crates/fbuild-serial/src/port_class.rs`,
`crates/fbuild-serial/src/boards.rs::family_for_vid_pid`, or any
FastLED/boards catalogue ingestion/family classification, or any
DTR/RTS handling in `SharedSerialManager::open_port`. See
FastLED/fbuild#899 (resolved).

Expand Down
19 changes: 11 additions & 8 deletions agents/docs/deploy-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,17 @@ The follow-up issues track the full polymorphic version:

The right sequence today:

1. **VID:PID first.** Add the new board's USB endpoint to
`crates/fbuild-serial/src/boards.rs::BOARD_FINGERPRINTS`. If the
data port is a *different* USB endpoint from the bootloader, add
`ENVIRONMENT_TO_VCOM` too.
2. **Family classification.** Add the new VID:PID to
`family_for_vid_pid`. RP2350 is almost certainly `CdcAcmBridge`
(1200-bps touch reset, host-ready idle); inherit the existing
RP2040 row's conventions.
1. **VID:PID first.** Publish the board's bootloader and runtime USB
endpoints in [FastLED/boards](https://github.com/FastLED/boards), then
exercise fbuild's normal catalogue ingestion path. Never add a literal
VID/PID to `BOARD_FINGERPRINTS`, `ENVIRONMENT_TO_VCOM`, generated Rust, or
any other production fallback in this repository.
2. **Family classification.** Resolve the published FastLED/boards product
identity and map that metadata to `CdcAcmBridge` (or the appropriate
family behavior). If the catalogue cannot express a needed distinction,
extend its published schema instead of embedding another ID table in
fbuild. RP2350 normally uses 1200-bps touch and host-ready idle, matching
the RP2040 convention.
3. **DTR/RTS matrix.** Add a row to
[`docs/usb-cdc-control-line-matrix.md`](../../docs/usb-cdc-control-line-matrix.md)
for the new chip, with a datasheet citation and capture date.
Expand Down
16 changes: 9 additions & 7 deletions agents/docs/serial-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ when you need to validate one of:
- A change to `crates/fbuild-serial/src/port_class.rs` — the OS-side
kernel-class detection introduced in #895. Sysfs paths can drift
between kernel versions; real-device validation catches it.
- A new entry in the `crates/fbuild-serial/src/boards.rs::family_for_vid_pid`
table — the disagreement warning shipped in #897 will fire here if
the table's implied class disagrees with what the kernel actually
binds.
- A new FastLED/boards USB catalogue record or data-driven family-classifier
change. The disagreement warning shipped in #897 will fire if the catalogue
identity's implied class disagrees with what the kernel actually binds. Never
add a production VID/PID literal to the legacy table.
- Anything that touches `SharedSerialManager::open_port`'s DTR/RTS
handling — getting `(false, false)` vs `(true, true)` wrong on
attach is the difference between "firmware runs" and "firmware
Expand Down Expand Up @@ -75,7 +75,8 @@ The script:
3. Ensures `cdc_acm` is loaded in Ubuntu.
4. Builds + runs a small Rust program (the source code is in the
script as a here-doc) that calls `port_class::detect_port_kernel_class`,
`family_for_vid_pid`, and `family_for_port` against `/dev/ttyACM0`,
the catalogue-backed family lookup and `family_for_port` against
`/dev/ttyACM0`,
and asserts both signals return `Esp32NativeUsbCdc` / `CdcAcm`.
5. `usbipd detach` (user-mode) — leaves the bind in place for the
next session.
Expand Down Expand Up @@ -121,7 +122,8 @@ Expected last line:
a "cdc_acm 1-1:1.0: ttyACM0: USB ACM device" line.
3. Verify `readlink /sys/class/tty/ttyACM0/device/driver` resolves
to `.../bus/usb/drivers/cdc_acm`. If it resolves to something
else, the VID/PID table or `port_class::linux::classify_driver`
may need updating.
else, the FastLED/boards record/ingestion or
`port_class::linux::classify_driver` may need updating. Do not patch in a
literal VID/PID fallback.
4. Worst case, the kernel driver name changed across mainline Linux
versions — bump the Microsoft WSL kernel and re-test.
97 changes: 92 additions & 5 deletions crates/fbuild-build-arm/src/rp2040/orchestrator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ impl BuildOrchestrator for Rp2040Orchestrator {
let toolchain_dir = fbuild_packages::Package::ensure_installed(&toolchain).await?;
tracing::info!("rp2040 pqt-gcc toolchain at {}", toolchain_dir.display());

// Arduino-Pico generates its canonical UF2 from the linked ELF with
// the managed pqt-picotool package. Do the same here rather than
// flattening ELF segments in an fbuild-specific encoder.
let picotool = fbuild_packages::toolchain::Rp2040Picotool::new(&params.project_dir);
let picotool_dir = fbuild_packages::Package::ensure_installed(&picotool).await?;
tracing::info!("managed picotool at {}", picotool_dir.display());

use fbuild_packages::Toolchain;
pipeline::log_toolchain_version(
&toolchain.get_gcc_path(),
Expand Down Expand Up @@ -134,12 +141,20 @@ impl BuildOrchestrator for Rp2040Orchestrator {
crate::eh_frame_policy::EhFramePolicy::Preserve => "preserve",
},
})?;
let (fast_elf, [fast_bin], fast_compile_db) =
expected_fast_path_artifacts(&build_dir, &params.project_dir, ["firmware.bin"]);
let (fast_elf, [fast_bin, fast_uf2], fast_compile_db) = expected_fast_path_artifacts(
&build_dir,
&params.project_dir,
["firmware.bin", "firmware.uf2"],
);
let fast_path = FastPathContract::for_project_outputs(
&build_dir,
&params.project_dir,
[fast_elf.clone(), fast_bin.clone(), fast_compile_db.clone()],
[
fast_elf.clone(),
fast_bin,
fast_uf2.clone(),
fast_compile_db.clone(),
],
);

if !params.compiledb_only
Expand All @@ -159,7 +174,7 @@ impl BuildOrchestrator for Rp2040Orchestrator {
let elapsed = start.elapsed().as_secs_f64();
return Ok(BuildResult {
success: true,
firmware_path: Some(fast_bin),
firmware_path: Some(fast_uf2),
elf_path: Some(fast_elf),
size_info: hit.size_info,
symbol_map: None,
Expand Down Expand Up @@ -324,7 +339,8 @@ impl BuildOrchestrator for Rp2040Orchestrator {
support_link_inputs.push(boot2_object);

// 9. Run shared sequential build pipeline
let build_result = pipeline::run_sequential_build_with_libs(
let board_mcu = ctx.board.mcu.clone();
let mut build_result = pipeline::run_sequential_build_with_libs(
&compiler,
&linker,
ctx,
Expand All @@ -338,6 +354,23 @@ impl BuildOrchestrator for Rp2040Orchestrator {
)
.await?;

if build_result.success && !params.compiledb_only {
let elf = build_result.elf_path.as_deref().ok_or_else(|| {
fbuild_core::FbuildError::BuildFailed(
"RP2040 link succeeded without a firmware ELF artifact".to_string(),
)
})?;
let uf2 = elf.with_extension("uf2");
convert_elf_to_uf2(
&picotool.executable(),
elf,
&uf2,
&board_mcu,
)
.await?;
build_result.firmware_path = Some(uf2);
}

if build_result.success
&& !params.compiledb_only
&& !params.symbol_analysis
Expand All @@ -358,6 +391,60 @@ impl BuildOrchestrator for Rp2040Orchestrator {
}
}

async fn convert_elf_to_uf2(
picotool: &Path,
elf: &Path,
uf2: &Path,
mcu: &str,
) -> Result<()> {
let family = if mcu.to_ascii_lowercase().starts_with("rp2350") {
"rp2350-arm-s"
} else {
"rp2040"
};
let mut args = vec![
picotool.to_string_lossy().to_string(),
"uf2".to_string(),
"convert".to_string(),
elf.to_string_lossy().to_string(),
uf2.to_string_lossy().to_string(),
"--family".to_string(),
family.to_string(),
];
if family.starts_with("rp2350") {
args.push("--abs-block".to_string());
}
let args_ref: Vec<&str> = args.iter().map(String::as_str).collect();
let output = fbuild_core::subprocess::run_command(
&args_ref,
None,
None,
Some(std::time::Duration::from_secs(30)),
)
.await?;
if !output.success() {
return Err(fbuild_core::FbuildError::BuildFailed(format!(
"managed picotool could not convert {} to {} for {family}: {}{}{}",
elf.display(),
uf2.display(),
output.stderr.trim(),
if output.stderr.is_empty() || output.stdout.is_empty() {
""
} else {
"\n"
},
output.stdout.trim()
)));
}
if !uf2.is_file() {
return Err(fbuild_core::FbuildError::BuildFailed(format!(
"managed picotool reported success without creating {}",
uf2.display()
)));
}
Ok(())
}

/// Create an RP2040 orchestrator.
pub fn create() -> Box<dyn BuildOrchestrator> {
Box::new(Rp2040Orchestrator)
Expand Down
2 changes: 1 addition & 1 deletion crates/fbuild-core/src/usb/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ pub fn try_install_online_cache(path: &Path) -> bool {
};
let mut packed: HashMap<u32, UsbInfo> = HashMap::with_capacity(parsed.len() * 4);
for (vid_str, value) in parsed {
let Some(vid) = parse_hex_u16(&vid_str) else {
let Some(vid) = parse_hex_u16(vid_str) else {
continue;
};
// Accept fbuild's legacy {vendor, products:[[pid,name]]} shape and
Expand Down
102 changes: 97 additions & 5 deletions crates/fbuild-daemon/src/handlers/operations/deploy_port.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,48 @@ pub(super) fn choose_deploy_port(
};
}

// RP2040/RP2350 stock boards deploy through the ROM BOOTSEL volume and
// may have no application CDC port at all before the UF2 copy. Never
// fall back to an unrelated serial endpoint (for example Windows COM1);
// the RP2040 deployer discovers the post-flash CDC port itself.
// A stock/blank Pico may have no CDC port, but a previously flashed Pico
// needs its catalogue-identified CDC port passed through for the 1200-bps
// reset. Never select by a built-in VID or fall back to an unrelated COM
// port: FastLED/boards data is the sole identity source.
if platform == Platform::RaspberryPi {
let expected_generation = board
.map(|board| board.mcu.to_ascii_lowercase())
.filter(|mcu| mcu.starts_with("rp2350"))
.map_or(RpGeneration::Rp2040, |_| RpGeneration::Rp2350);
let mut matches: Vec<_> = devices
.into_iter()
.filter(|device| device.is_connected)
.filter_map(|device| {
let identity = device
.vid
.zip(device.pid)
.and_then(|(vid, pid)| fbuild_core::usb::try_resolve(vid, pid));
identity_matches_rp_generation(identity.as_ref(), expected_generation).then_some(PortCandidate {
port: device.port,
vid: device.vid,
pid: device.pid,
description: device.description,
})
})
.collect();
matches.sort_by(|a, b| a.port.cmp(&b.port));
if matches.len() == 1 {
log_connect("deploy", &matches[0]);
return DeployPortChoice {
port: Some(matches[0].port.clone()),
warning: None,
};
}
if matches.len() > 1 {
return DeployPortChoice {
port: None,
warning: Some(format!(
"multiple FastLED/boards-identified Raspberry Pi CDC ports are connected: {}; pass -p/--port to select the deployment target",
format_candidates(matches.iter())
)),
};
}
return DeployPortChoice {
port: None,
warning: None,
Expand Down Expand Up @@ -107,6 +144,32 @@ pub(super) fn choose_deploy_port(
}
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum RpGeneration {
Rp2040,
Rp2350,
}

fn identity_matches_rp_generation(
identity: Option<&fbuild_core::usb::UsbInfo>,
expected: RpGeneration,
) -> bool {
classify_rp_generation(identity) == Some(expected)
}

fn classify_rp_generation(
identity: Option<&fbuild_core::usb::UsbInfo>,
) -> Option<RpGeneration> {
let product = identity?.product.to_ascii_lowercase();
if product.contains("rp2350") || product.contains("pico 2") {
Some(RpGeneration::Rp2350)
} else if product.contains("rp2040") || product.contains("raspberry pi pico") {
Some(RpGeneration::Rp2040)
} else {
None
}
}

pub(super) fn append_warning_to_stderr(stderr: &mut Option<String>, warning: Option<String>) {
let Some(warning) = warning else {
return;
Expand All @@ -133,7 +196,9 @@ fn expected_vids(platform: Platform, board: Option<&BoardConfig>) -> Vec<u16> {
Platform::Espressif32 => &[0x303A],
Platform::AtmelAvr | Platform::AtmelMegaAvr => &[0x2341, 0x2A03, 0x1A86, 0x10C4, 0x0403],
Platform::NxpLpc => &[0x1FC9, 0x0D28],
Platform::RaspberryPi => &[0x2E8A],
// Raspberry Pi identities are consumed from the FastLED/boards USB
// catalogue by the RP2040 deployer. Never embed a family VID here.
Platform::RaspberryPi => &[],
_ => &[],
};

Expand Down Expand Up @@ -248,6 +313,33 @@ mod tests {
assert!(choice.warning.is_none());
}

#[test]
fn raspberry_pi_identity_is_catalogue_driven() {
let pico = fbuild_core::usb::UsbInfo {
// Dataset aggregation can preserve a framework/vendor label here;
// generation comes from the curated product identity.
vendor: "Arduino".to_string(),
product: "Raspberry Pi Pico".to_string(),
};
let pico2 = fbuild_core::usb::UsbInfo {
vendor: "Arduino".to_string(),
product: "Raspberry Pi Pico 2".to_string(),
};
assert!(identity_matches_rp_generation(
Some(&pico),
RpGeneration::Rp2040
));
assert!(!identity_matches_rp_generation(
Some(&pico2),
RpGeneration::Rp2040
));
assert!(identity_matches_rp_generation(
Some(&pico2),
RpGeneration::Rp2350
));
assert!(!identity_matches_rp_generation(None, RpGeneration::Rp2040));
}

#[test]
fn selects_single_matching_teensy_vid() {
let choice = choose_deploy_port(
Expand Down
Loading
Loading