Skip to content

Commit bf7c9f4

Browse files
authored
Show house component, bom sourcability info (#290)
* Add support for parsing "matcher" Indicate house parts in blue in `pcb bom` and `pcb ipc2581 bom`. * Fix dnp propagantion bug * Fix formatting, update bom snapshots * Unify bom table rendering, indicate DNP with color * Add qty, remove dnp column * Parse generic data from IPC bom * Add generic absorption to bom processing When a matched generic is more strict than another generic, it should absorb it to reduce unique bom entries. * Sort designators * Feature gate bom_table
1 parent ca9e69c commit bf7c9f4

25 files changed

+1591
-262
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ pcb-eda = { path = "crates/pcb-eda" }
2929
pcb-layout = { path = "crates/pcb-layout" }
3030
pcb-sch = { path = "crates/pcb-sch" }
3131
pcb-zen = { path = "crates/pcb-zen" }
32-
pcb-zen-core = { path = "crates/pcb-zen-core", default-features = true }
32+
pcb-zen-core = { path = "crates/pcb-zen-core" }
3333
pcb-ui = { path = "crates/pcb-ui" }
3434
pcb-kicad = { path = "crates/pcb-kicad" }
3535
pcb-sexpr = { path = "crates/pcb-sexpr", features = ["serde"] }

crates/pcb-ipc2581-tools/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ colored = { workspace = true }
1414
comfy-table = { workspace = true }
1515
ipc2581 = { workspace = true }
1616
log = { workspace = true }
17-
pcb-sch = { workspace = true }
17+
pcb-sch = { workspace = true, features = ["table"] }
1818
serde = { workspace = true }
1919
serde_json = { workspace = true }
2020
starlark_syntax = { workspace = true }

crates/pcb-ipc2581-tools/src/commands/bom.rs

Lines changed: 77 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,39 @@ use std::io::{self, Write};
33
use std::path::Path;
44

55
use anyhow::Result;
6-
use comfy_table::presets::UTF8_FULL_CONDENSED;
7-
use comfy_table::Table;
86
use pcb_sch::{Bom, BomEntry};
9-
use starlark_syntax::slice_vec_ext::SliceExt;
107

118
use crate::utils::file as file_utils;
129
use crate::OutputFormat;
1310

11+
/// Trim and truncate description to 100 chars max
12+
fn trim_description(s: Option<String>) -> Option<String> {
13+
s.map(|s| {
14+
let trimmed = s.trim();
15+
if trimmed.len() > 100 {
16+
format!("{} ...", &trimmed[..96])
17+
} else {
18+
trimmed.to_string()
19+
}
20+
})
21+
.filter(|s| !s.is_empty())
22+
}
23+
1424
/// Extracted characteristics data from IPC-2581 BOM items
1525
#[derive(Debug, Default)]
1626
pub struct CharacteristicsData {
1727
pub package: Option<String>,
1828
pub value: Option<String>,
1929
pub path: Option<String>,
30+
pub matcher: Option<String>,
2031
pub alternatives: Vec<pcb_sch::Alternative>,
2132
pub properties: std::collections::BTreeMap<String, String>,
33+
pub component_type: Option<String>,
34+
pub resistance: Option<String>,
35+
pub capacitance: Option<String>,
36+
pub voltage: Option<String>,
37+
pub dielectric: Option<String>,
38+
pub esr: Option<String>,
2239
}
2340

2441
/// Extract characteristics from IPC-2581 Characteristics
@@ -40,11 +57,19 @@ pub fn extract_characteristics(
4057
"package" | "footprint" => data.package = Some(val_str),
4158
"value" => data.value = Some(val_str),
4259
"path" => data.path = Some(val_str),
60+
"matcher" => data.matcher = Some(val_str),
4361
"alternatives" => {
4462
if let Some(alternative) = parse_alternative_json(&val_str) {
4563
data.alternatives.push(alternative);
4664
}
4765
}
66+
// Generic component fields
67+
"type" => data.component_type = Some(val_str.to_lowercase()),
68+
"resistance" => data.resistance = Some(val_str),
69+
"capacitance" => data.capacitance = Some(val_str),
70+
"voltage" => data.voltage = Some(val_str),
71+
"dielectric" => data.dielectric = Some(val_str),
72+
"esr" => data.esr = Some(val_str),
4873
// Exclude well-known fields (MPN/Manufacturer come from AVL)
4974
// and instance-specific metadata
5075
"mpn"
@@ -80,6 +105,34 @@ fn parse_alternative_json(json_str: &str) -> Option<pcb_sch::Alternative> {
80105
Some(pcb_sch::Alternative { mpn, manufacturer })
81106
}
82107

108+
/// Build GenericComponent from extracted characteristics
109+
/// Reuses the same logic as detect_generic_component in pcb-sch
110+
fn build_generic_component(data: &CharacteristicsData) -> Option<pcb_sch::GenericComponent> {
111+
match data.component_type.as_deref()? {
112+
"resistor" => {
113+
let resistance = data.resistance.as_ref()?.parse().ok()?;
114+
let voltage = data.voltage.as_ref().and_then(|v| v.parse().ok());
115+
Some(pcb_sch::GenericComponent::Resistor(pcb_sch::Resistor {
116+
resistance,
117+
voltage,
118+
}))
119+
}
120+
"capacitor" => {
121+
let capacitance = data.capacitance.as_ref()?.parse().ok()?;
122+
let dielectric = data.dielectric.as_ref().and_then(|d| d.parse().ok());
123+
let esr = data.esr.as_ref().and_then(|e| e.parse().ok());
124+
let voltage = data.voltage.as_ref().and_then(|v| v.parse().ok());
125+
Some(pcb_sch::GenericComponent::Capacitor(pcb_sch::Capacitor {
126+
capacitance,
127+
dielectric,
128+
esr,
129+
voltage,
130+
}))
131+
}
132+
_ => None,
133+
}
134+
}
135+
83136
pub fn execute(file: &Path, format: OutputFormat) -> Result<()> {
84137
let content = file_utils::load_ipc_file(file)?;
85138
let ipc = ipc2581::Ipc2581::parse(&content)?;
@@ -93,7 +146,7 @@ pub fn execute(file: &Path, format: OutputFormat) -> Result<()> {
93146
write!(writer, "{}", bom.ungrouped_json())?;
94147
}
95148
OutputFormat::Text => {
96-
write_bom_table(&bom, writer)?;
149+
bom.write_table(writer)?;
97150
}
98151
};
99152

@@ -114,45 +167,53 @@ fn extract_bom_from_ipc(ipc: &ipc2581::Ipc2581) -> Result<Bom> {
114167
}
115168

116169
// Extract characteristics from BomItem
170+
let characteristics_data = item
171+
.characteristics
172+
.as_ref()
173+
.map(|chars| extract_characteristics(ipc, chars))
174+
.unwrap_or_default();
175+
117176
let CharacteristicsData {
118177
package,
119178
value,
120179
path: component_path,
180+
matcher,
121181
alternatives: textual_alternatives,
122182
properties,
123-
} = item
124-
.characteristics
125-
.as_ref()
126-
.map(|chars| extract_characteristics(ipc, chars))
127-
.unwrap_or_default();
183+
..
184+
} = &characteristics_data;
128185

129186
// AVL provides canonical MPN and Manufacturer via Enterprise references
130187
let (mpn, manufacturer, avl_alternatives) =
131188
lookup_from_avl(ipc, item.oem_design_number_ref);
132189

133190
// Merge alternatives: AVL takes precedence, then textual characteristics
134191
let mut alternatives = avl_alternatives;
135-
alternatives.extend(textual_alternatives);
192+
alternatives.extend(textual_alternatives.clone());
136193

137194
// Use BomItem description attribute if present, otherwise fallback to value
138195
let description = item
139196
.description
140197
.map(|sym| ipc.resolve(sym).to_string())
141198
.or(value.clone());
142199

200+
// Build generic component data if available
201+
let generic_data = build_generic_component(&characteristics_data);
202+
143203
// Build entry
144204
let entry = BomEntry {
145205
mpn,
146206
alternatives,
147207
manufacturer,
148-
package,
149-
value,
150-
description,
151-
generic_data: None,
208+
package: package.clone(),
209+
value: value.clone(),
210+
description: trim_description(description),
211+
generic_data,
152212
offers: Vec::new(),
153213
dnp: false, // Will be set per ref_des
154214
skip_bom: false,
155-
properties,
215+
matcher: matcher.clone(),
216+
properties: properties.clone(),
156217
};
157218

158219
// Process reference designators
@@ -203,6 +264,7 @@ fn extract_bom_from_ipc(ipc: &ipc2581::Ipc2581) -> Result<Bom> {
203264
offers: Vec::new(),
204265
dnp: false,
205266
skip_bom: false,
267+
matcher: None,
206268
properties: std::collections::BTreeMap::new(),
207269
};
208270

@@ -269,89 +331,3 @@ pub fn lookup_from_avl(
269331

270332
(primary_mpn, primary_manufacturer, alternatives)
271333
}
272-
273-
fn write_bom_table<W: Write>(bom: &Bom, mut writer: W) -> io::Result<()> {
274-
let mut table = Table::new();
275-
table.load_preset(UTF8_FULL_CONDENSED);
276-
table.set_content_arrangement(comfy_table::ContentArrangement::DynamicFullWidth);
277-
278-
let json: serde_json::Value = serde_json::from_str(&bom.grouped_json()).unwrap();
279-
for entry in json.as_array().unwrap() {
280-
let designators = entry["designators"]
281-
.as_array()
282-
.unwrap()
283-
.map(|d| d.as_str().unwrap())
284-
.join(",");
285-
286-
// Use first offer info if available, otherwise use base component info
287-
let (mpn, manufacturer) = entry
288-
.get("offers")
289-
.and_then(|o| o.as_array())
290-
.and_then(|arr| {
291-
arr.iter()
292-
.find(|offer| offer["distributor"].as_str() != Some("__AVL__"))
293-
})
294-
.map(|offer| {
295-
(
296-
offer["manufacturer_pn"].as_str().unwrap_or_default(),
297-
offer["manufacturer"].as_str().unwrap_or_default(),
298-
)
299-
})
300-
.unwrap_or_else(|| {
301-
(
302-
entry["mpn"].as_str().unwrap_or_default(),
303-
entry["manufacturer"].as_str().unwrap_or_default(),
304-
)
305-
});
306-
307-
// Use description field if available, otherwise use value
308-
let description = entry["description"]
309-
.as_str()
310-
.or_else(|| entry["value"].as_str())
311-
.unwrap_or_default();
312-
313-
// Get alternatives if present
314-
let alternatives_str = entry
315-
.get("alternatives")
316-
.and_then(|a| a.as_array())
317-
.map(|arr| {
318-
if arr.is_empty() {
319-
String::new()
320-
} else {
321-
arr.iter()
322-
.filter_map(|alt| alt["mpn"].as_str())
323-
.collect::<Vec<_>>()
324-
.join(", ")
325-
}
326-
})
327-
.unwrap_or_default();
328-
329-
table.add_row(vec![
330-
designators.as_str(),
331-
mpn,
332-
manufacturer,
333-
entry["package"].as_str().unwrap_or_default(),
334-
description,
335-
alternatives_str.as_str(),
336-
if entry["dnp"].as_bool().unwrap_or(false) {
337-
"Yes"
338-
} else {
339-
"No"
340-
},
341-
]);
342-
}
343-
344-
// Set headers
345-
table.set_header(vec![
346-
"Designators",
347-
"MPN",
348-
"Manufacturer",
349-
"Package",
350-
"Description",
351-
"Alternatives",
352-
"DNP",
353-
]);
354-
355-
writeln!(writer, "{table}")?;
356-
Ok(())
357-
}

crates/pcb-sch/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ starlark = { workspace = true }
2626
starlark_map = { workspace = true }
2727
allocative = { workspace = true }
2828
anyhow = { workspace = true }
29+
comfy-table = { workspace = true, optional = true }
30+
starlark_syntax = { workspace = true }
31+
32+
[features]
33+
default = []
34+
table = ["comfy-table"]
2935

3036
[dev-dependencies]
3137
tempfile = { workspace = true }

0 commit comments

Comments
 (0)