@@ -3,22 +3,39 @@ use std::io::{self, Write};
33use std:: path:: Path ;
44
55use anyhow:: Result ;
6- use comfy_table:: presets:: UTF8_FULL_CONDENSED ;
7- use comfy_table:: Table ;
86use pcb_sch:: { Bom , BomEntry } ;
9- use starlark_syntax:: slice_vec_ext:: SliceExt ;
107
118use crate :: utils:: file as file_utils;
129use 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 ) ]
1626pub 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+
83136pub 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- }
0 commit comments