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
116 changes: 111 additions & 5 deletions crates/fbuild-serial/src/bootloader_watcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,15 @@ impl BootloaderSignature {
/// `SamdUf2` is VID-only (PID varies); the others are exact-pair.
#[must_use]
pub fn matches(&self, vid: u16, pid: u16) -> bool {
match self {
BootloaderSignature::Rp2040BootSel => (vid, pid) == (0x2E8A, 0x0003),
BootloaderSignature::SamdDfu => (vid, pid) == (0x03EB, 0x6124),
BootloaderSignature::SamdUf2 => vid == 0x239A,
BootloaderSignature::TeensyHidBootloader => (vid, pid) == (0x16C0, 0x0478),
#[cfg(test)]
{
test_signature_match(*self, vid, pid)
}
#[cfg(not(test))]
{
fbuild_core::usb::profiles::profiles_for(vid, pid)
.iter()
.any(|profile| profile_matches_signature(*self, profile))
}
}

Expand All @@ -84,6 +88,55 @@ impl BootloaderSignature {
}
}

fn profile_matches_signature(
signature: BootloaderSignature,
profile: &fbuild_core::usb::profiles::UsbTransportProfile,
) -> bool {
use fbuild_core::usb::profiles::{UsbDeviceRole, UsbPurpose};

match signature {
BootloaderSignature::Rp2040BootSel => {
profile.purpose == UsbPurpose::Bootloader
&& profile.role == UsbDeviceRole::BootloaderUf2
&& profile.family.as_deref() == Some("rp2040")
}
BootloaderSignature::SamdDfu => {
profile.purpose == UsbPurpose::Bootloader
&& matches!(
profile.role,
UsbDeviceRole::BootloaderDfu | UsbDeviceRole::RecoveryTransport
)
&& profile
.family
.as_deref()
.is_some_and(|family| family.contains("sam"))
}
BootloaderSignature::SamdUf2 => {
profile.purpose == UsbPurpose::Bootloader
&& profile.role == UsbDeviceRole::BootloaderUf2
&& profile
.family
.as_deref()
.is_some_and(|family| family.contains("samd"))
}
BootloaderSignature::TeensyHidBootloader => {
profile.purpose == UsbPurpose::Bootloader
&& profile.role == UsbDeviceRole::BootloaderHid
&& profile.family.as_deref() == Some("teensy")
}
}
}

#[cfg(test)]
fn test_signature_match(signature: BootloaderSignature, vid: u16, pid: u16) -> bool {
match signature {
BootloaderSignature::Rp2040BootSel => (vid, pid) == (0x2E8A, 0x0003),
BootloaderSignature::SamdDfu => (vid, pid) == (0x03EB, 0x6124),
BootloaderSignature::SamdUf2 => vid == 0x239A,
BootloaderSignature::TeensyHidBootloader => (vid, pid) == (0x16C0, 0x0478),
}
}

/// A lightweight `(vid, pid, name)` snapshot of one USB serial /
/// MSC / HID port. Decoupled from `serialport::SerialPortInfo` so
/// tests can drive the source without standing up a real port.
Expand Down Expand Up @@ -205,6 +258,59 @@ mod tests {
use super::*;
use std::cell::Cell;

fn boot_profile(
role: fbuild_core::usb::profiles::UsbDeviceRole,
family: &str,
) -> fbuild_core::usb::profiles::UsbTransportProfile {
use fbuild_core::usb::profiles::{
UsbIdentityMatch, UsbProfileProvenance, UsbPurpose, UsbTransportProfile,
};
UsbTransportProfile {
identity_match: UsbIdentityMatch {
vid: "feed".to_string(),
pid: Some("c0de".to_string()),
pid_mask: None,
},
purpose: UsbPurpose::Bootloader,
role,
transport: "usb".to_string(),
reset: "manual".to_string(),
handoff: "bootloader".to_string(),
platform: None,
family: Some(family.to_string()),
generation: None,
interface: None,
provenance: UsbProfileProvenance {
source_url: "test://fixture".to_string(),
source_revision: "a".repeat(40),
source_class: "test".to_string(),
},
priority: 100,
allow_ambiguous: false,
}
}

#[test]
fn typed_profiles_drive_bootloader_signatures_without_identity_constants() {
use fbuild_core::usb::profiles::UsbDeviceRole;

let uf2 = boot_profile(UsbDeviceRole::BootloaderUf2, "rp2040");
assert!(profile_matches_signature(
BootloaderSignature::Rp2040BootSel,
&uf2
));
assert!(!profile_matches_signature(
BootloaderSignature::TeensyHidBootloader,
&uf2
));

let teensy = boot_profile(UsbDeviceRole::BootloaderHid, "teensy");
assert!(profile_matches_signature(
BootloaderSignature::TeensyHidBootloader,
&teensy
));
}

/// Scripted source — returns the snapshot at index `i` on the
/// i'th call. Tests use this to model "port appears at T+1
/// poll" / "never appears" / "already present at T=0."
Expand Down
3 changes: 1 addition & 2 deletions docs/usb-vidpid-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ catalogue blob (`EMBEDDED_PROTO` is empty outside tests).
| `crates/fbuild-core/src/usb/data.rs` (`MANIFEST_URL`, protobuf/JSON cache boundary) | Canonical ingestion | Runtime cache populated from FastLED/boards; legacy JSON URL remains compatibility-only and must be removed after consumers migrate. |
| `crates/fbuild-core/data/usb-vendors.tar.zst` + `crates/fbuild-core/src/usb/embedded.rs` | Test-only fixture | The module, archive extraction dependency, and fallback are compiled only under `cfg(test)`. Release/runtime resolution uses the verified FastLED/boards cache or an explicit unknown-device label. |
| `crates/fbuild-serial/src/boards.rs` (`BOARD_FINGERPRINTS`, `ENVIRONMENT_TO_VCOM`, `family_for_vid_pid`) | Migrated; test fixtures remain | Production hints, VCOM selection, and reset-family classification now derive from verified typed FastLED/boards profiles. Concrete tables and range matching are compiled only under `cfg(test)`. |
| `crates/fbuild-serial/src/bootloader_watcher.rs` | Legacy bootloader VID/PID signatures | RP2040/SAMD/Teensy bootloader detection still uses concrete signatures; boards metadata needs a bootloader identity/role field before this can become data-driven. |
| `crates/fbuild-serial/src/bootloader_watcher.rs` | Migrated; test fixtures remain | Production RP2040/SAMD/Teensy bootloader detection resolves the verified typed FastLED/boards profiles and matches their bootloader purpose, transport role, and family. Concrete signatures are compiled only under `cfg(test)`. |
| `crates/fbuild-daemon/src/handlers/operations/deploy_port.rs` | Legacy runtime VID fallback | Expected vendor IDs are deploy-port selection heuristics, not names; they still duplicate identity knowledge and require boards-derived upload metadata before removal. |
| `crates/fbuild-deploy/src/lpc_debugger_reflash.rs` | Protocol/device compatibility constants | LPC-Link2 firmware recovery requires the exact probe identity. Provenance is NXP/FastLED LPC-Link2 documentation; move to boards metadata when the probe schema supports non-board recovery targets. |
| `crates/fbuild-deploy/src/probe_rs.rs`, `crates/fbuild-deploy/src/teensy/port_discovery.rs` | Legacy runtime probe/loader matching | Probe and HalfKay discovery have explicit VID/PID signatures; these need published role records or a protocol-level classifier before removal. |
Expand All @@ -38,7 +38,6 @@ above (test-only assertions are intentionally omitted):
| Path | Pairs |
| --- | --- |
| `crates/fbuild-serial/src/boards.rs` | `1FC9:0132`, `16C0:0483`, `303A:1001`, `303A:0002`, `10C4:EA60`, `10C4:EA70`, `1A86:7523`, `1A86:55D4`, `0403:6001`, `0403:6015`, `2341:0043`, `2341:0001`, `2341:0010`, `2341:804E`, `2E8A:000A`, `2E8A:0003` |
| `crates/fbuild-serial/src/bootloader_watcher.rs` | `2E8A:0003`, `03EB:6124`, `239A:*`, `16C0:0478` |
| `crates/fbuild-daemon/src/handlers/operations/deploy_port.rs` | `16C0:*`, `303A:*`, `2341:*`, `2A03:*`, `1A86:*`, `10C4:*`, `0403:*`, `1FC9:*`, `0D28:*`, plus test-only concrete rows |
| `crates/fbuild-deploy/src/lpc_debugger_reflash.rs` | `1FC9:0132` |
| `crates/fbuild-deploy/src/probe_rs.rs` | `1FC9:0090`, `1FC9:0132` |
Expand Down
Loading