632 lines
20 KiB
Rust
632 lines
20 KiB
Rust
#[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<T, F>(label: &str, mut op: F) -> Result<T, KiCadError>
|
|
where
|
|
F: FnMut() -> Result<T, KiCadError>,
|
|
{
|
|
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<i32> {
|
|
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<String> {
|
|
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<String, String> {
|
|
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::<Vec<_>>()
|
|
.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<dyn std::error::Error>> {
|
|
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<String> = 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<String> = selected_footprints
|
|
.iter()
|
|
.filter_map(|fp| fp.reference.clone())
|
|
.collect();
|
|
let selected_fp_ids: BTreeSet<String> = 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<String, BTreeSet<String>> = 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::<Vec<_>>().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<i32> = [
|
|
"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<PcbItem> = 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);
|
|
}
|