#[cfg(feature = "blocking")] use std::collections::{BTreeMap, BTreeSet}; #[cfg(feature = "blocking")] use std::thread::sleep; #[cfg(feature = "blocking")] use std::time::Duration; #[cfg(feature = "blocking")] use kicad_ipc_rs::{ KiCadClient, KiCadClientBlocking, KiCadError, PcbArc, PcbBoardGraphicShape, PcbBoardText, PcbBoardTextBox, PcbDimension, PcbField, PcbFootprint, PcbGroup, PcbItem, PcbPad, PcbTrack, PcbVia, PcbZone, }; #[cfg(feature = "blocking")] fn retry(label: &str, mut op: F) -> Result where F: FnMut() -> Result, { let attempts = 6; for attempt in 1..=attempts { match op() { Ok(value) => return Ok(value), Err(err) => { if attempt == attempts { return Err(err); } eprintln!("warn: {label} attempt {attempt} failed: {err}"); sleep(Duration::from_millis(300)); } } } unreachable!("attempt loop exits via return"); } #[cfg(feature = "blocking")] fn item_id(item: &PcbItem) -> Option<&str> { match item { PcbItem::Track(v) => v.id.as_deref(), PcbItem::Arc(v) => v.id.as_deref(), PcbItem::Via(v) => v.id.as_deref(), PcbItem::Footprint(v) => v.id.as_deref(), PcbItem::Pad(v) => v.id.as_deref(), PcbItem::BoardGraphicShape(v) => v.id.as_deref(), PcbItem::BoardText(v) => v.id.as_deref(), PcbItem::BoardTextBox(v) => v.id.as_deref(), PcbItem::Field(_) => None, PcbItem::Zone(v) => v.id.as_deref(), PcbItem::Dimension(v) => v.id.as_deref(), PcbItem::Group(v) => v.id.as_deref(), PcbItem::Unknown(_) => None, } } #[cfg(feature = "blocking")] fn pcb_type_code(name: &str) -> Option { KiCadClient::pcb_object_type_codes() .iter() .find(|entry| entry.name == name) .map(|entry| entry.code) } #[cfg(feature = "blocking")] fn extract_property(line: &str, key: &str) -> Option { let marker = format!("(property \"{key}\" \""); let start = line.find(&marker)?; let rest = &line[start + marker.len()..]; let end = rest.find('"')?; Some(rest[..end].to_string()) } #[cfg(feature = "blocking")] fn parse_ref_values_from_selection_sexpr(contents: &str) -> BTreeMap { let mut refs = Vec::new(); let mut vals = Vec::new(); for line in contents.lines() { if let Some(reference) = extract_property(line, "Reference") { refs.push(reference); } if let Some(value) = extract_property(line, "Value") { vals.push(value); } } let mut out = BTreeMap::new(); let limit = refs.len().min(vals.len()); for index in 0..limit { out.insert(refs[index].clone(), vals[index].clone()); } out } #[cfg(feature = "blocking")] fn print_item(item: &PcbItem) { match item { PcbItem::Track(PcbTrack { id, start_nm, end_nm, width_nm, layer, net, .. }) => { println!( " track id={} start={:?} end={:?} width_nm={:?} layer={} net={}", id.as_deref().unwrap_or("-"), start_nm.map(|v| (v.x_nm, v.y_nm)), end_nm.map(|v| (v.x_nm, v.y_nm)), width_nm, layer.name, net.as_ref().map(|v| v.name.as_str()).unwrap_or("-") ); } PcbItem::Arc(PcbArc { id, start_nm, mid_nm, end_nm, width_nm, layer, net, .. }) => { println!( " arc id={} start={:?} mid={:?} end={:?} width_nm={:?} layer={} net={}", id.as_deref().unwrap_or("-"), start_nm.map(|v| (v.x_nm, v.y_nm)), mid_nm.map(|v| (v.x_nm, v.y_nm)), end_nm.map(|v| (v.x_nm, v.y_nm)), width_nm, layer.name, net.as_ref().map(|v| v.name.as_str()).unwrap_or("-") ); } PcbItem::Via(PcbVia { id, position_nm, via_type, layers, net, .. }) => { let stack = layers .as_ref() .map(|l| { l.padstack_layers .iter() .map(|x| x.name.clone()) .collect::>() .join(",") }) .unwrap_or_else(|| "-".to_string()); println!( " via id={} pos={:?} via_type={:?} layers={} net={}", id.as_deref().unwrap_or("-"), position_nm.map(|v| (v.x_nm, v.y_nm)), via_type, stack, net.as_ref().map(|v| v.name.as_str()).unwrap_or("-") ); } PcbItem::Footprint(PcbFootprint { id, reference, position_nm, orientation_deg, layer, pad_count, .. }) => { println!( " footprint id={} ref={} pos={:?} orientation_deg={:?} layer={} pad_count={}", id.as_deref().unwrap_or("-"), reference.as_deref().unwrap_or("-"), position_nm.map(|v| (v.x_nm, v.y_nm)), orientation_deg, layer.name, pad_count ); } PcbItem::Pad(PcbPad { id, number, pad_type, position_nm, net, .. }) => { println!( " pad id={} number={} pad_type={:?} pos={:?} net={}", id.as_deref().unwrap_or("-"), number, pad_type, position_nm.map(|v| (v.x_nm, v.y_nm)), net.as_ref().map(|v| v.name.as_str()).unwrap_or("-") ); } PcbItem::BoardGraphicShape(PcbBoardGraphicShape { id, layer, net, geometry_kind, .. }) => { println!( " shape id={} layer={} geometry={} net={}", id.as_deref().unwrap_or("-"), layer.name, geometry_kind.as_deref().unwrap_or("-"), net.as_ref().map(|v| v.name.as_str()).unwrap_or("-") ); } PcbItem::BoardText(PcbBoardText { id, layer, text, .. }) => { println!( " text id={} layer={} text={}", id.as_deref().unwrap_or("-"), layer.name, text.as_deref().unwrap_or("-") ); } PcbItem::BoardTextBox(PcbBoardTextBox { id, layer, text, .. }) => { println!( " textbox id={} layer={} text={}", id.as_deref().unwrap_or("-"), layer.name, text.as_deref().unwrap_or("-") ); } PcbItem::Field(PcbField { name, visible, text, }) => { println!( " field name={} visible={} text={}", name, visible, text.as_deref().unwrap_or("-") ); } PcbItem::Zone(PcbZone { id, name, zone_type, layer_count, filled, polygon_count, .. }) => { println!( " zone id={} name={} zone_type={:?} layers={} filled={} polygons={}", id.as_deref().unwrap_or("-"), name, zone_type, layer_count, filled, polygon_count ); } PcbItem::Dimension(PcbDimension { id, layer, text, style_kind, .. }) => { println!( " dimension id={} layer={} style={} text={}", id.as_deref().unwrap_or("-"), layer.name, style_kind.as_deref().unwrap_or("-"), text.as_deref().unwrap_or("-") ); } PcbItem::Group(PcbGroup { id, name, item_count, .. }) => { println!( " group id={} name={} item_count={}", id.as_deref().unwrap_or("-"), name, item_count ); } PcbItem::Unknown(v) => { println!(" unknown type={} raw_len={}", v.type_url, v.raw_len); } } } #[cfg(feature = "blocking")] fn main() -> Result<(), Box> { let client = KiCadClientBlocking::connect()?; retry("ping", || client.ping())?; let version = retry("get_version", || client.get_version())?; println!("version={}", version.full_version); let summary = retry("get_selection_summary", || { client.get_selection_summary(Vec::new()) })?; println!("selection_total={}", summary.total_items); for count in summary.type_url_counts { println!( "selection_type type_url={} count={}", count.type_url, count.count ); } if summary.total_items == 0 { println!("selection is empty; nothing to inspect"); return Ok(()); } let selected_items = retry("get_selection", || client.get_selection(Vec::new()))?; let selected_details = retry("get_selection_details", || { client.get_selection_details(Vec::new()) })?; let selected_raw = retry("get_selection_raw", || client.get_selection_raw(Vec::new()))?; println!("selected_items_decoded={}", selected_items.len()); for item in &selected_items { print_item(item); } println!("selected_items_raw={}", selected_raw.len()); for (idx, item) in selected_raw.iter().enumerate() { println!( " raw[{idx}] type_url={} raw_len={}", item.type_url, item.value.len() ); } println!("selected_items_detail_rows={}", selected_details.len()); for (idx, row) in selected_details.iter().enumerate() { println!( " detail[{idx}] type_url={} raw_len={} detail={}", row.type_url, row.raw_len, row.detail ); } let selected_ids: Vec = selected_items .iter() .filter_map(item_id) .map(str::to_string) .collect(); println!("selected_ids={}", selected_ids.len()); for id in &selected_ids { println!(" id={id}"); } if !selected_ids.is_empty() { let bboxes = retry("get_item_bounding_boxes", || { client.get_item_bounding_boxes(selected_ids.clone(), true) })?; println!("item_bounding_boxes={}", bboxes.len()); for bbox in bboxes { println!( " bbox id={} x_nm={} y_nm={} width_nm={} height_nm={}", bbox.item_id, bbox.x_nm, bbox.y_nm, bbox.width_nm, bbox.height_nm ); } } let mut selected_footprints = Vec::new(); for item in &selected_items { if let PcbItem::Footprint(fp) = item { selected_footprints.push(fp.clone()); } } let selected_refs: BTreeSet = selected_footprints .iter() .filter_map(|fp| fp.reference.clone()) .collect(); let selected_fp_ids: BTreeSet = selected_footprints .iter() .filter_map(|fp| fp.id.clone()) .collect(); println!("selected_footprints={}", selected_footprints.len()); for fp in &selected_footprints { println!( " footprint ref={} id={} pos={:?} orientation_deg={:?} layer={} pad_count={}", fp.reference.as_deref().unwrap_or("-"), fp.id.as_deref().unwrap_or("-"), fp.position_nm.map(|v| (v.x_nm, v.y_nm)), fp.orientation_deg, fp.layer.name, fp.pad_count ); } match retry("get_selection_as_string", || { client.get_selection_as_string() }) { Ok(selection_dump) => { println!("selection_string_ids={}", selection_dump.ids.len()); let ref_values = parse_ref_values_from_selection_sexpr(&selection_dump.contents); println!("ref_value_pairs={}", ref_values.len()); for (reference, value) in ref_values { println!(" {reference} => {value}"); } } Err(err) => { println!("ref_value_pairs unavailable: {err}"); } } let pad_netlist = retry("get_pad_netlist", || client.get_pad_netlist())?; let mut selected_pad_rows = Vec::new(); for row in pad_netlist { let by_ref = row .footprint_reference .as_ref() .map(|r| selected_refs.contains(r)) .unwrap_or(false); let by_id = row .footprint_id .as_ref() .map(|id| selected_fp_ids.contains(id)) .unwrap_or(false); if by_ref || by_id { selected_pad_rows.push(row); } } println!("selected_pad_rows={}", selected_pad_rows.len()); let mut selected_net_names = BTreeSet::new(); let mut net_to_selected_refs: BTreeMap> = BTreeMap::new(); for row in &selected_pad_rows { let reference = row .footprint_reference .as_deref() .map(str::to_string) .unwrap_or_else(|| "-".to_string()); let net = row .net_name .as_deref() .map(str::to_string) .unwrap_or_else(|| "-".to_string()); println!( " pad ref={} fp_id={} pad_id={} pad_number={} net_code={:?} net_name={}", reference, row.footprint_id.as_deref().unwrap_or("-"), row.pad_id.as_deref().unwrap_or("-"), row.pad_number, row.net_code, net ); if net != "-" { selected_net_names.insert(net.clone()); if reference != "-" { net_to_selected_refs .entry(net) .or_default() .insert(reference); } } } println!("selected_net_names={}", selected_net_names.len()); for net in &selected_net_names { println!(" net={net}"); } println!("interconnections_among_selected_refs"); for (net, refs) in &net_to_selected_refs { if refs.len() >= 2 { println!( " net={} refs={}", net, refs.iter().cloned().collect::>().join(",") ); } } let all_nets = retry("get_nets", || client.get_nets())?; let mut name_to_code = BTreeMap::new(); for net in all_nets { name_to_code.insert(net.name, net.code); } let mut selected_net_codes = Vec::new(); for name in &selected_net_names { if let Some(code) = name_to_code.get(name) { selected_net_codes.push(*code); } } selected_net_codes.sort_unstable(); selected_net_codes.dedup(); println!("selected_net_codes={:?}", selected_net_codes); let route_type_codes: Vec = [ "KOT_PCB_TRACE", "KOT_PCB_VIA", "KOT_PCB_ARC", "KOT_PCB_ZONE", "KOT_PCB_PAD", "KOT_PCB_SHAPE", ] .into_iter() .filter_map(pcb_type_code) .collect(); println!("route_type_codes={route_type_codes:?}"); if !selected_net_codes.is_empty() && !route_type_codes.is_empty() { match retry("get_items_by_net", || { client.get_items_by_net(route_type_codes.clone(), selected_net_codes.clone()) }) { Ok(connected_items) => { let mut counts: BTreeMap<&'static str, usize> = BTreeMap::new(); for item in &connected_items { let label = match item { PcbItem::Track(_) => "track", PcbItem::Via(_) => "via", PcbItem::Arc(_) => "arc", PcbItem::Zone(_) => "zone", PcbItem::Pad(_) => "pad", _ => "other", }; *counts.entry(label).or_insert(0) += 1; } println!("connected_items_on_selected_nets={}", connected_items.len()); for (label, count) in counts { println!(" connected_{label}s={count}"); } for item in connected_items.iter().take(80) { print_item(item); } } Err(err) => { println!("connected_items_on_selected_nets unavailable via GetItemsByNet: {err}"); println!("connected_items fallback: scan-by-type + local net-name filter"); let mut items = Vec::new(); for code in &route_type_codes { match retry("get_items_by_type_code_fallback", || { client.get_items_by_type_codes(vec![*code]) }) { Ok(mut bucket) => items.append(&mut bucket), Err(type_err) => { println!(" fallback type_code={code} unavailable: {type_err}"); } } } if items.is_empty() { println!("connected_items fallback unavailable: no items from type scans"); } else { let filtered: Vec = items .into_iter() .filter(|item| match item { PcbItem::Track(track) => track .net .as_ref() .map(|n| selected_net_names.contains(&n.name)) .unwrap_or(false), PcbItem::Via(via) => via .net .as_ref() .map(|n| selected_net_names.contains(&n.name)) .unwrap_or(false), PcbItem::Arc(arc) => arc .net .as_ref() .map(|n| selected_net_names.contains(&n.name)) .unwrap_or(false), PcbItem::Pad(pad) => pad .net .as_ref() .map(|n| selected_net_names.contains(&n.name)) .unwrap_or(false), PcbItem::BoardGraphicShape(shape) => shape .net .as_ref() .map(|n| selected_net_names.contains(&n.name)) .unwrap_or(false), _ => false, }) .collect(); let mut counts: BTreeMap<&'static str, usize> = BTreeMap::new(); for item in &filtered { let label = match item { PcbItem::Track(_) => "track", PcbItem::Via(_) => "via", PcbItem::Arc(_) => "arc", PcbItem::Pad(_) => "pad", PcbItem::BoardGraphicShape(_) => "shape", _ => "other", }; *counts.entry(label).or_insert(0) += 1; } println!( "connected_items_on_selected_nets_fallback={}", filtered.len() ); for (label, count) in counts { println!(" connected_{label}s={count}"); } for item in filtered.iter().take(120) { print_item(item); } } } } } else { println!("connected_items_on_selected_nets unavailable: no resolvable net codes"); } Ok(()) } #[cfg(not(feature = "blocking"))] fn main() { eprintln!("run with --features blocking"); std::process::exit(1); }