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
92 changes: 60 additions & 32 deletions crates/fbuild-core/src/usb/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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://github.com/ghraw/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://github.com/ghraw/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://github.com/ghraw/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<Option<HashMap<u32, UsbInfo>>> = RwLock::new(None);

Expand All @@ -85,7 +81,13 @@ static ONLINE_MAP: RwLock<Option<HashMap<u32, UsbInfo>>> = 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
Expand Down Expand Up @@ -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.
Expand All @@ -211,7 +203,7 @@ pub fn try_install_online_cache(path: &Path) -> bool {
return false;
}
};
let parsed: HashMap<String, VendorEntry> = 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");
Expand All @@ -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<u32, UsbInfo> = 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();
Expand Down Expand Up @@ -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();
}
}
13 changes: 12 additions & 1 deletion crates/fbuild-deploy/src/rp2040.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -39,7 +42,7 @@ pub fn encode_uf2_for_family(binary: &[u8], family_id: u32) -> Vec<u8> {
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.
Expand Down Expand Up @@ -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
Expand Down
Loading