app
adom-schematic-viewer
Public Made by Adomby adom
Interactive schematic viewer: your EDA's own render plus per-symbol highlighting and live MPN, stock and price on hover
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
//! Component enrichment: MPN → { price, stock, tier, popularity, wiki_url }.
//!
//! Sources (sibling Adom CLIs, shelled with a hard timeout so a slow/absent CLI
//! never hangs the UI):
//! • popularity + wiki page — `adom-wiki discover search --query <mpn> --json`
//! (vouch_count / star_count → popularity; owner/slug → wiki_url)
//! • price + stock — `adom-parts-search search <mpn>` (max stock across
//! vendors; first real unit price; JLCPCB tier from flags)
//!
//! Because the vendor lookup is slow (~30 s, live APIs), the server PRE-WARMS the
//! cache for every board MPN in a background thread at load time; hover then hits
//! the cache. `lookup()` is the (slow) cache-fill; `/enrich` reads the result.
use serde_json::{json, Value};
use std::process::{Command, Stdio};
use std::sync::mpsc;
use std::time::Duration;
/// Run a command, capping wall-clock at `secs`. Returns stdout on success.
fn run_capped(bin: &str, args: &[&str], secs: u64) -> Option<String> {
let bin = bin.to_string();
let args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
let out = Command::new(&bin).args(&args).stdin(Stdio::null()).output();
let _ = tx.send(out.ok().filter(|o| o.status.success()).map(|o| String::from_utf8_lossy(&o.stdout).into_owned()));
});
rx.recv_timeout(Duration::from_secs(secs)).ok().flatten()
}
/// Strip any leading non-JSON preamble (parts-search prints an "OK: …" summary
/// line before its JSON) and parse.
fn parse_json_tail(s: &str) -> Option<Value> {
let start = s.find(['{', '['])?;
serde_json::from_str(&s[start..]).ok()
}
const JLCPCB_DEFAULT: &str = "https://jlcpcb-wela51osctvp.adom.cloud";
/// Enrich a schematic component: JLCPCB stock/price/tier by LCSC part number
/// (the molecule parts carry `LCSC PN` C-numbers), plus a wiki page ONLY on a
/// close name match (so passives don't match random pages). `value` is the
/// symbol Value (e.g. "BME690"); `lcsc` is the LCSC C-number.
pub fn lookup(value: &str, lcsc: &str) -> String {
let mut out = json!({});
let o = out.as_object_mut().unwrap();
let mut mpn = String::new(); // real MPN, learned from JLCPCB
// ── JLCPCB by LCSC PN — stock / price / tier / popularity / mfr ──
let is_lcsc = lcsc.starts_with('C') && lcsc.len() > 1 && lcsc[1..].chars().all(|c| c.is_ascii_digit());
if is_lcsc {
let api = std::env::var("JLCPCB_API").unwrap_or_else(|_| JLCPCB_DEFAULT.to_string());
if let Some(txt) = run_capped("adom-jlcpcb", &["search", lcsc, "--limit", "1", "--api", &api], 20) {
if let Some(v) = parse_json_tail(&txt) {
if let Some(c) = v.get("components").and_then(|c| c.as_array()).and_then(|a| a.first()) {
if let Some(s) = c.get("stock").and_then(|x| x.as_i64()) { o.insert("stock".into(), json!(s)); }
if let Some(p) = c.get("unit_price_usd").and_then(|x| x.as_f64()) { o.insert("price".into(), json!(format!("${:.4}", p))); }
let tier = if c.get("is_basic").and_then(|x| x.as_bool()).unwrap_or(false) { Some("Basic") }
else if c.get("is_preferred").and_then(|x| x.as_bool()).unwrap_or(false) { Some("Preferred") } else { Some("Extended") };
if let Some(t) = tier { o.insert("tier".into(), json!(t)); }
if let Some(pop) = c.get("popularity").and_then(|x| x.as_i64()) { o.insert("popularity".into(), json!(pop)); }
if let Some(u) = c.get("jlcpcb_url").and_then(|x| x.as_str()) { o.insert("jlc_url".into(), json!(u)); }
if let Some(m) = c.get("mfr").and_then(|x| x.as_str()) { mpn = m.to_string(); }
if let Some(mf) = c.get("manufacturer").and_then(|x| x.as_str()) { o.insert("mfr".into(), json!(mf)); }
}
}
}
}
// ── wiki page — ONLY on a close name match (avoids garbage for passives) ──
let wq = if !mpn.is_empty() { mpn.clone() } else { value.to_string() };
if wq.len() > 2 {
if let Some(txt) = run_capped("adom-wiki", &["discover", "search", "--query", &wq, "--limit", "5", "--json"], 12) {
if let Some(v) = parse_json_tail(&txt) {
if let Some(results) = v.get("data").and_then(|d| d.get("results")).and_then(|r| r.as_array()) {
let ql = wq.to_lowercase();
let pick = results.iter().find(|r| {
let t = r.get("title").and_then(|t| t.as_str()).unwrap_or("").to_lowercase();
t.len() > 2 && (t == ql || ql.contains(&t) || t.contains(&ql))
});
if let Some(r) = pick {
let owner = r.get("owner").and_then(|x| x.as_str()).unwrap_or("");
let slug = r.get("slug").and_then(|x| x.as_str()).unwrap_or("");
if !slug.is_empty() { o.insert("wiki_url".into(), json!(format!("https://wiki.adom.inc/{owner}/{slug}"))); }
if !o.contains_key("popularity") {
let vouch = r.get("vouch_count").and_then(|x| x.as_i64()).unwrap_or(0);
o.insert("popularity".into(), json!(vouch));
}
}
}
}
}
}
// the real manufacturer part number (learned from JLCPCB) — the abridged
// hover card shows this instead of the schematic's `value`.
if !mpn.is_empty() { o.insert("mpn".into(), json!(mpn)); }
if o.is_empty() { return "{\"error\":\"no data\"}".into(); }
out.to_string()
}