From 1961996b2e2e639e29a90a3ea6a6f47e17d7c1d5 Mon Sep 17 00:00:00 2001 From: zackees Date: Tue, 14 Jul 2026 20:37:45 -0700 Subject: [PATCH] refactor(serial): derive bootloader signatures from board profiles --- .../fbuild-serial/src/bootloader_watcher.rs | 116 +++++++++++++++++- docs/usb-vidpid-audit.md | 3 +- 2 files changed, 112 insertions(+), 7 deletions(-) diff --git a/crates/fbuild-serial/src/bootloader_watcher.rs b/crates/fbuild-serial/src/bootloader_watcher.rs index ad08d25f8..8674c50a1 100644 --- a/crates/fbuild-serial/src/bootloader_watcher.rs +++ b/crates/fbuild-serial/src/bootloader_watcher.rs @@ -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)) } } @@ -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. @@ -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." diff --git a/docs/usb-vidpid-audit.md b/docs/usb-vidpid-audit.md index 4dc977500..edda539b2 100644 --- a/docs/usb-vidpid-audit.md +++ b/docs/usb-vidpid-audit.md @@ -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. | @@ -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` |