app
Adom Concur
Public Made by Adomby adom
Cross-source consensus check for chip libraries — scores whether the KiCad, EAGLE, and Altium libraries AND the adom-ds2sf datasheet extraction all AGREE on pins, pads, names, and package, across seve
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
//! Altium `.PcbLib` (and `.IntLib`'s embedded PcbLib stream) adapter.
//!
//! Wraps the shared `altium-pcblib` crate and adapts its rich pad JSON down
//! to concur's lean [`Part`] shape — concur only needs pad designators +
//! the footprint name to score consensus, so we drop position / size /
//! shape / drill on the floor.
//!
//! ## Multi-variant selection
//!
//! Many vendor `.PcbLib`s ship multiple footprint variants in one file —
//! e.g. DRV8316RRGFR carries `RGF0040E-IPC_A` (41-pad IPC standard, the
//! one that matches the chip's actual pinout) and `RGF0040E-MFG` (57-pad
//! manufacturing layout with extra test points and fiducials). The
//! upstream `altium-pcblib` crate picks one as "primary" by stream order
//! and shoves the rest into `_alternates`. That order isn't guaranteed
//! to give us the IPC variant, and getting the wrong one silently
//! breaks the consensus check (DRV8316 v0.4 → divergent because altium
//! reported 57 pads vs kicad+eagle's 41).
//!
//! [`parse_with_consensus_hint`] walks all the variants (primary +
//! alternates) and picks the one whose pad-count is closest to the
//! `consensus_pad_count` hint passed in by cli.rs (computed from the
//! other parsed sources). If no consensus is available yet, falls back
//! to the primary the upstream picked.
use anyhow::{Context as _, Result};
use std::path::Path;
use super::{Pad, PackageInfo, Part, Pin};
#[derive(Debug, Clone)]
struct AltiumVariant {
module_name: Option<String>,
pads: Vec<Pad>,
parse_error: Option<String>,
}
/// Parse without any consensus hint (caller doesn't know what other sources
/// say yet). Returns the primary footprint as upstream chose it. Kept as
/// public for callers that don't have other sources to reason against.
#[allow(dead_code)]
pub fn parse(path: &Path) -> Result<Part> {
parse_with_consensus_hint(path, None, None)
}
/// Read the Altium `.SchLib` symbol pins (number + name) via `altium-schlib`,
/// so the Altium source contributes to the symbol-side axes (pin count, pin
/// names, pin→pad map) — not just footprint pads. Best-effort: a missing or
/// unparseable `.SchLib` just yields no pins (footprint-only, as before).
pub fn parse_schlib_pins(schlib: &Path) -> Vec<Pin> {
let Ok(sym) = altium_schlib::parse_altium_schlib_path(schlib) else { return Vec::new() };
let Some(arr) = sym.get("pins").and_then(|p| p.as_array()) else { return Vec::new() };
arr.iter()
.filter_map(|p| {
let number = p.get("number").and_then(|n| n.as_str())?.to_string();
// Altium SchLib pins carry per-char overbar backslashes (C\S\); strip
// them so names compare apples-to-apples with KiCad/EAGLE/ds2sf.
let name = p.get("name").and_then(|n| n.as_str()).unwrap_or("").replace('\\', "");
if number.is_empty() { return None; }
let is_stub_name = name.is_empty() || name == number;
Some(Pin { number, name, is_stub_name })
})
.collect()
}
/// Parse and pick the variant whose pad count is closest to
/// `consensus_pad_count`. When `Some`, walks primary + every alternate.
/// `schlib` (when present) supplies the symbol-side pins.
pub fn parse_with_consensus_hint(
path: &Path,
consensus_pad_count: Option<usize>,
schlib: Option<&Path>,
) -> Result<Part> {
let parsed = altium_pcblib::parse_altium_pcblib_path(path)
.with_context(|| format!("altium-pcblib parsing {}", path.display()))?;
let mut variants: Vec<AltiumVariant> = Vec::new();
variants.push(extract_variant(&parsed));
if let Some(alts) = parsed.get("_alternates").and_then(|v| v.as_array()) {
for alt in alts {
variants.push(extract_variant(alt));
}
}
// Reject variants that didn't parse (parse_error AND no pads) — they
// can't contribute to a consensus and would just add noise.
variants.retain(|v| !v.pads.is_empty() || v.parse_error.is_none());
if variants.is_empty() {
anyhow::bail!("no parseable footprint variants in PcbLib");
}
let chosen = match consensus_pad_count {
Some(target) => {
// Pick the variant whose pad count is closest to target.
// Tiebreak: prefer the one whose name doesn't scream "MFG" /
// "DEBUG" / "TEST" / "FIDUCIAL" (manufacturing layouts with
// extra non-pinout pads).
variants
.iter()
.min_by_key(|v| {
let diff = v.pads.len().abs_diff(target);
let name_penalty = v
.module_name
.as_ref()
.map(|n| {
let upper = n.to_uppercase();
if upper.contains("-MFG") || upper.contains("_MFG")
|| upper.contains("DEBUG") || upper.contains("FIDUCIAL")
|| upper.contains("TEST")
{
100usize
} else {
0
}
})
.unwrap_or(0);
diff * 1000 + name_penalty
})
.expect("variants is non-empty")
.clone()
}
None => variants[0].clone(),
};
if chosen.pads.is_empty() {
if let Some(err_msg) = chosen.parse_error.as_ref() {
anyhow::bail!("chosen variant failed to parse: {err_msg}");
}
}
let package = chosen.module_name.clone().map(|raw_name| PackageInfo {
raw_name,
body_x_mm: None,
body_y_mm: None,
});
let pins = schlib.map(parse_schlib_pins).unwrap_or_default();
Ok(Part {
source: "altium".to_string(),
source_part_name: chosen.module_name,
pads: chosen.pads,
pins,
package,
})
}
fn extract_variant(v: &serde_json::Value) -> AltiumVariant {
let module_name = v.get("module_name").and_then(|x| x.as_str()).map(String::from);
let pads: Vec<Pad> = v
.get("pads")
.and_then(|x| x.as_array())
.map(|arr| {
arr.iter()
.filter_map(|p| p.get("number").and_then(|n| n.as_str()).map(String::from))
.map(|number| Pad { number })
.collect()
})
.unwrap_or_default();
let parse_error = v.get("_parse_error").and_then(|x| x.as_str()).map(String::from);
AltiumVariant { module_name, pads, parse_error }
}