app
Adom DS2SF - Datasheet to Symbol and Footprint
Public Made by Adomby adom
Datasheet to symbol + footprint + provenance JSON — pins and pads, extracted from the PDF.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
use anyhow::Result;
use serde::{Deserialize, Serialize};
const KICAD_BASE: &str = "https://kicad-rk5ue5pcfemi.adom.cloud";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Resolved {
pub input: String,
pub kicad_baseline: Option<KicadBaseline>,
pub search_attempts: Vec<SearchAttempt>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KicadBaseline {
pub library: String,
pub name: String,
pub source: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchAttempt {
pub library: String,
pub name: String,
pub result: String,
}
/// Resolve a free-form package string against the service-kicad standard library.
/// Tries a hand-curated normalize map first, then probes service-kicad for each candidate.
pub fn resolve(package: &str, info_json_hint: Option<&str>) -> Result<Resolved> {
let combined = match info_json_hint {
Some(h) => format!("{package} | {h}"),
None => package.to_string(),
};
let mut candidates = candidate_map(&combined);
// Dedup while preserving order.
let mut seen = std::collections::HashSet::new();
candidates.retain(|c| seen.insert(format!("{}:{}", c.0, c.1)));
let mut attempts = Vec::new();
for (lib, name) in &candidates {
let result = probe_kicad(lib, name);
attempts.push(SearchAttempt {
library: lib.clone(),
name: name.clone(),
result: result.clone(),
});
if result == "200" {
return Ok(Resolved {
input: package.to_string(),
kicad_baseline: Some(KicadBaseline {
library: lib.clone(),
name: name.clone(),
source: "service-kicad-stdlib".to_string(),
}),
search_attempts: attempts,
});
}
}
Ok(Resolved {
input: package.to_string(),
kicad_baseline: None,
search_attempts: attempts,
})
}
fn probe_kicad(library: &str, name: &str) -> String {
let url = format!("{KICAD_BASE}/fp/export/svg/{library}/{name}");
match ureq::get(&url)
.timeout(std::time::Duration::from_secs(4))
.call()
{
Ok(r) if r.status() == 200 => "200".to_string(),
Ok(r) => r.status().to_string(),
Err(ureq::Error::Status(code, _)) => code.to_string(),
Err(e) => format!("err:{e}"),
}
}
/// Generate plausible (library, name) candidates for a free-form package string.
/// Hand-curated heuristics; expand as we observe misses on real datasheets.
fn candidate_map(s: &str) -> Vec<(String, String)> {
let upper = s.to_uppercase();
let mut out: Vec<(String, String)> = Vec::new();
// VSSOP / MSOP small-outline.
if upper.contains("VSSOP-10") || upper.contains("VSSOP10") || upper.contains("MSOP-10") {
out.push(("Package_SO".into(), "VSSOP-10_3x3mm_P0.5mm".into()));
out.push(("Package_SO".into(), "MSOP-10_3x3mm_P0.5mm".into()));
}
if upper.contains("VSSOP-8") || upper.contains("MSOP-8") {
out.push(("Package_SO".into(), "VSSOP-8_3.0x3.0mm_P0.65mm".into()));
out.push(("Package_SO".into(), "MSOP-8_3x3mm_P0.65mm".into()));
}
// SOIC narrow / wide.
if upper.contains("SOIC-8") || upper.contains("SO-8") || upper.contains("SOIC8") {
if upper.contains("WIDE") || upper.contains("208") || upper.contains("5.3X5.3") {
out.push(("Package_SO".into(), "SOIC-8W_5.3x5.3mm_P1.27mm".into()));
} else {
out.push(("Package_SO".into(), "SOIC-8_3.9x4.9mm_P1.27mm".into()));
}
}
if upper.contains("SOIC-14") {
out.push(("Package_SO".into(), "SOIC-14_3.9x8.7mm_P1.27mm".into()));
}
if upper.contains("SOIC-16") {
if upper.contains("WIDE") || upper.contains("7.5") {
out.push(("Package_SO".into(), "SOIC-16W_7.5x10.3mm_P1.27mm".into()));
} else {
out.push(("Package_SO".into(), "SOIC-16_3.9x9.9mm_P1.27mm".into()));
}
}
// SOT-23 family.
if upper.contains("SOT-23-5") || upper.contains("SOT23-5") || upper.contains("SOT-25") {
out.push(("Package_TO_SOT_SMD".into(), "SOT-23-5".into()));
}
if upper.contains("SOT-23-6") || upper.contains("SOT23-6") {
out.push(("Package_TO_SOT_SMD".into(), "SOT-23-6".into()));
}
if upper.contains("SOT-23") && !upper.contains("-5") && !upper.contains("-6") {
out.push(("Package_TO_SOT_SMD".into(), "SOT-23".into()));
}
if upper.contains("SOT-563") {
out.push(("Package_TO_SOT_SMD".into(), "SOT-563".into()));
}
if upper.contains("SOT-25") {
out.push(("Package_TO_SOT_SMD".into(), "SOT-25".into()));
}
// QFN / VQFN / DFN.
if let Some((pin_count, body)) = parse_qfn(&upper) {
// Try with EP (most QFNs have one), then without.
out.push((
"Package_DFN_QFN".into(),
format!("QFN-{pin_count}-1EP_{body}_P0.5mm_EP3.2x3.2mm"),
));
out.push((
"Package_DFN_QFN".into(),
format!("QFN-{pin_count}-1EP_{body}_P0.4mm_EP3.2x3.2mm"),
));
out.push((
"Package_DFN_QFN".into(),
format!("QFN-{pin_count}_{body}_P0.5mm"),
));
}
// LQFP / TQFP.
if let Some((pin_count, body)) = parse_lqfp(&upper) {
out.push((
"Package_LQFP".into(),
format!("LQFP-{pin_count}_{body}_P0.5mm"),
));
out.push((
"Package_QFP".into(),
format!("TQFP-{pin_count}_{body}_P0.5mm"),
));
}
// BGA — very lookup-heavy; just register the search attempt for now.
if upper.contains("BGA") {
if let Some(p) = parse_bga(&upper) {
out.push(("Package_BGA".into(), p));
}
}
// DIP / PDIP.
if upper.contains("DIP-8") || upper.contains("PDIP-8") {
out.push(("Package_DIP".into(), "DIP-8_W7.62mm".into()));
}
if upper.contains("DIP-14") || upper.contains("PDIP-14") {
out.push(("Package_DIP".into(), "DIP-14_W7.62mm".into()));
}
if upper.contains("DIP-16") || upper.contains("PDIP-16") {
out.push(("Package_DIP".into(), "DIP-16_W7.62mm".into()));
}
// OLGA / non-standard LGA — not in stdlib; leave blank so caller falls through.
// (Empty candidate list means kicad_baseline will be None.)
out
}
fn parse_qfn(upper: &str) -> Option<(u32, String)> {
// Match patterns like "QFN-32 5x5", "VQFN-32 (5×5)", "VQFN32-5X5", "QFN 32 5X5MM"
let pin_count = find_after(upper, "QFN").or_else(|| find_after(upper, "QFN-"))?;
let body = parse_body_dims(upper)?;
Some((pin_count, body))
}
fn parse_lqfp(upper: &str) -> Option<(u32, String)> {
let key = if upper.contains("LQFP") {
"LQFP"
} else if upper.contains("TQFP") {
"TQFP"
} else {
return None;
};
let pin_count = find_after(upper, key)?;
let body = parse_body_dims(upper)?;
Some((pin_count, body))
}
fn parse_bga(upper: &str) -> Option<String> {
let pin_count = find_after(upper, "BGA")?;
Some(format!("BGA-{pin_count}"))
}
fn find_after(s: &str, prefix: &str) -> Option<u32> {
let idx = s.find(prefix)?;
let rest = &s[idx + prefix.len()..];
let rest = rest.trim_start_matches(['-', ' ']);
let digits: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
if digits.is_empty() {
None
} else {
digits.parse().ok()
}
}
fn parse_body_dims(upper: &str) -> Option<String> {
// Find patterns like "5X5", "5x5", "7X7", "5×5", "10×10".
// Boundary rule: the char before the first digit MUST be non-digit
// (so "VQFN-32 5X5" → "5x5", not "325x5"; "32" gets boundary-rejected).
let s = upper.replace('×', "X");
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
// Boundary check before reading a number.
let prev_ok = i == 0
|| !(bytes[i - 1].is_ascii_digit() || bytes[i - 1] == b'.');
if prev_ok && bytes[i].is_ascii_digit() {
let n1_start = i;
while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'.') {
i += 1;
}
let n1 = &s[n1_start..i];
if i < bytes.len() && bytes[i] == b'X' {
i += 1;
let n2_start = i;
while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'.') {
i += 1;
}
if i > n2_start {
let n2 = &s[n2_start..i];
if let (Ok(a), Ok(b)) = (n1.parse::<f64>(), n2.parse::<f64>()) {
// Sanity: package body dims are typically 1–30 mm.
if (0.5..=50.0).contains(&a) && (0.5..=50.0).contains(&b) {
return Some(format!("{n1}x{n2}mm"));
}
}
}
}
} else {
i += 1;
}
}
None
}