diff --git a/crates/fbuild-core/src/usb/data.rs b/crates/fbuild-core/src/usb/data.rs
index 71a89dac..ae6dfb8b 100644
--- a/crates/fbuild-core/src/usb/data.rs
+++ b/crates/fbuild-core/src/usb/data.rs
@@ -56,25 +56,21 @@
use super::UsbInfo;
use prost::Message;
-use serde::Deserialize;
+use serde_json::Value;
use std::collections::HashMap;
use std::path::Path;
use std::sync::{OnceLock, RwLock};
use std::time::Duration;
-/// Legacy URL of the dataset index produced by the `online-data` branch's
-/// nightly workflow.
-pub const MANIFEST_URL: &str =
- "https://raw.githubusercontent.com/fastled/fbuild/online-data/manifest.json";
+/// FastLED/boards published registry (canonical USB VID:PID source).
+pub const MANIFEST_URL: &str = "https://fastled.github.io/boards/_meta.json";
/// Legacy JSON overlay URL. Kept for compatibility with older callers and
/// tests; new code should use [`USB_VIDS_PROTO_ZSTD_URL`].
-pub const USB_VID_JSON_URL: &str =
- "https://raw.githubusercontent.com/fastled/fbuild/online-data/data/usb-vid.json";
+pub const USB_VID_JSON_URL: &str = "https://fastled.github.io/boards/usb-ids.json";
/// Current compact USB VID:PID overlay published by the `online-data` branch.
-pub const USB_VIDS_PROTO_ZSTD_URL: &str =
- "https://raw.githubusercontent.com/fastled/fbuild/online-data/data/usb-vids.proto.zstd";
+pub const USB_VIDS_PROTO_ZSTD_URL: &str = "https://fastled.github.io/boards/usb-vids.proto.zstd";
static ONLINE_MAP: RwLock>> = RwLock::new(None);
@@ -85,7 +81,13 @@ static ONLINE_MAP: RwLock >> = RwLock::new(None);
/// (`builders/build_usb_ids.py` over the `platformio`/`arduino`/`vendors`/
/// `other` branches) and vendored here. Refresh workflow: see
/// `crates/fbuild-core/data/README.md`.
+// Production never ships a built-in VID/PID catalogue. The canonical
+// FastLED/boards artifact is fetched/ingested by the build/runtime cache
+// path. Keep the historical blob available only to unit tests as a fixture.
+#[cfg(test)]
const EMBEDDED_PROTO: &[u8] = include_bytes!("../../data/usb-vids.proto.zstd");
+#[cfg(not(test))]
+const EMBEDDED_PROTO: &[u8] = &[];
/// Both projections of the embedded proto. The compact `usb-vids.proto.zstd`
/// carries a `Vendor{vid, name, [Product{pid, name}]}` tree, so a single
@@ -184,16 +186,6 @@ struct Product {
name: String,
}
-/// On-disk representation: one entry per VID, with the vendor name shared
-/// across all products of that VID. Each product is a two-element
-/// `[pid_hex, product_name]` tuple.
-#[derive(Debug, Deserialize)]
-struct VendorEntry {
- vendor: String,
- #[serde(default)]
- products: Vec<(String, String)>,
-}
-
/// Install the overlay from a JSON file on disk. Replaces any previously
/// installed overlay. Silently no-ops on any IO or parse error so the
/// resolver never crashes on a stale / partial cache file.
@@ -211,7 +203,7 @@ pub fn try_install_online_cache(path: &Path) -> bool {
return false;
}
};
- let parsed: HashMap = match serde_json::from_str(&raw) {
+ let parsed: Value = match serde_json::from_str(&raw) {
Ok(m) => m,
Err(e) => {
tracing::warn!(?path, error = %e, "usb online overlay: parse failed");
@@ -220,22 +212,41 @@ pub fn try_install_online_cache(path: &Path) -> bool {
};
// Flatten the on-disk per-VID nested shape into the O(1) flat
// `(vid, pid) -> UsbInfo` lookup table the resolver expects.
+ let Some(parsed) = parsed.as_object() else {
+ tracing::warn!(?path, "usb online overlay: root is not an object");
+ return false;
+ };
let mut packed: HashMap = HashMap::with_capacity(parsed.len() * 4);
- for (vid_str, entry) in parsed {
+ for (vid_str, value) in parsed {
let Some(vid) = parse_hex_u16(&vid_str) else {
continue;
};
- for (pid_str, product_name) in entry.products {
- let Some(pid) = parse_hex_u16(&pid_str) else {
- continue;
- };
- packed.insert(
- pack(vid, pid),
- UsbInfo {
- vendor: entry.vendor.clone(),
- product: product_name,
- },
- );
+ // Accept fbuild's legacy {vendor, products:[[pid,name]]} shape and
+ // the canonical FastLED/boards {"Vendor name":..., "PIDs":[{pid:name}]}
+ // shape. Keeping this compatibility at the boundary lets boards remain
+ // the single source of truth without a second generated fbuild file.
+ let vendor = value.get("vendor").and_then(Value::as_str)
+ .or_else(|| value.get("Vendor name").and_then(Value::as_str))
+ .unwrap_or("Unknown USB vendor");
+ if let Some(products) = value.get("products").and_then(Value::as_array) {
+ for pair in products {
+ let Some(items) = pair.as_array() else { continue };
+ if items.len() != 2 { continue; }
+ let Some(pid_str) = items[0].as_str() else { continue };
+ let Some(product_name) = items[1].as_str() else { continue };
+ let Some(pid) = parse_hex_u16(pid_str) else { continue; };
+ packed.insert(pack(vid, pid), UsbInfo { vendor: vendor.to_string(), product: product_name.to_string() });
+ }
+ }
+ if let Some(products) = value.get("PIDs").and_then(Value::as_array) {
+ for item in products {
+ let Some(map) = item.as_object() else { continue };
+ for (pid_str, product_name) in map {
+ let Some(product_name) = product_name.as_str() else { continue };
+ let Some(pid) = parse_hex_u16(pid_str) else { continue; };
+ packed.insert(pack(vid, pid), UsbInfo { vendor: vendor.to_string(), product: product_name.to_string() });
+ }
+ }
}
}
let count = packed.len();
@@ -669,4 +680,21 @@ mod tests {
assert!(lookup(0xFEED, 0xC0DE).is_none());
clear_online_cache_for_tests();
}
+
+ #[test]
+ fn install_online_cache_accepts_fastled_boards_shape() {
+ let _guard = OVERLAY_LOCK.lock().unwrap();
+ clear_online_cache_for_tests();
+ let tmp = tempfile::tempdir().unwrap();
+ let path = tmp.path().join("boards-usb-ids.json");
+ std::fs::write(
+ &path,
+ r#"{"2e8a":{"Vendor name":"Raspberry Pi","PIDs":[{"0003":"Raspberry Pi RP2 BOOTSEL"},{"000a":"Raspberry Pi Pico"},{"000f":"Raspberry Pi Pico 2"}]}}"#,
+ )
+ .unwrap();
+ assert!(try_install_online_cache(&path));
+ assert_eq!(lookup(0x2e8a, 0x0003).unwrap().product, "Raspberry Pi RP2 BOOTSEL");
+ assert_eq!(lookup(0x2e8a, 0x000f).unwrap().product, "Raspberry Pi Pico 2");
+ clear_online_cache_for_tests();
+ }
}
diff --git a/crates/fbuild-deploy/src/rp2040.rs b/crates/fbuild-deploy/src/rp2040.rs
index fb80cfdc..fc716f59 100644
--- a/crates/fbuild-deploy/src/rp2040.rs
+++ b/crates/fbuild-deploy/src/rp2040.rs
@@ -19,6 +19,9 @@ const UF2_MAGIC_END: u32 = 0x0AB1_6F30;
const UF2_FLAG_FAMILY_ID_PRESENT: u32 = 0x0000_2000;
const RP2040_FAMILY_ID: u32 = 0xE48B_FF56;
const RP2350_FAMILY_ID: u32 = 0xE48B_FF59;
+// The RP2040 ROM UF2 converter addresses flash as an offset. The official
+// Arduino-Pico converter defaults BIN input to 0x2000, not the MCU XIP alias.
+const RP2040_UF2_BASE_ADDRESS: u32 = 0x0000_2000;
const UF2_PAYLOAD_SIZE: usize = 256;
const UF2_BLOCK_SIZE: usize = 512;
@@ -39,7 +42,7 @@ pub fn encode_uf2_for_family(binary: &[u8], family_id: u32) -> Vec {
put_u32(&mut block, 0, UF2_MAGIC_START0);
put_u32(&mut block, 4, UF2_MAGIC_START1);
put_u32(&mut block, 8, UF2_FLAG_FAMILY_ID_PRESENT);
- put_u32(&mut block, 12, start as u32 + 0x1000_0000);
+ put_u32(&mut block, 12, start as u32 + RP2040_UF2_BASE_ADDRESS);
// UF2 blocks always advertise a full 256-byte payload. The final
// block is zero-padded; advertising its short source length makes
// the ROM BOOTSEL parser reject an otherwise valid image.
@@ -359,6 +362,14 @@ mod tests {
u32::from_le_bytes(uf2[28..32].try_into().unwrap()),
RP2040_FAMILY_ID
);
+ assert_eq!(
+ u32::from_le_bytes(uf2[12..16].try_into().unwrap()),
+ RP2040_UF2_BASE_ADDRESS
+ );
+ assert_eq!(
+ u32::from_le_bytes(uf2[512 + 12..512 + 16].try_into().unwrap()),
+ RP2040_UF2_BASE_ADDRESS + UF2_PAYLOAD_SIZE as u32
+ );
assert_eq!(
u32::from_le_bytes(uf2[512 + 20..512 + 24].try_into().unwrap()),
1