app
Adom Chip Fetcher
Public Made by Adomby adom
Your whole parts library — manufacturer-grade chip CAD (symbol, footprint, 3D) one tap from your EDA tool.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
//! "Send to" delivery for a fetched chip — push its CAD straight to the user's
//! desktop EDA tools via the adom-desktop bridge. KiCad gets the native
//! `.kicad_sym` + `.kicad_mod`; Fusion 360 gets a true EAGLE `.lbr` (symbol +
//! footprint) built by adom-lbr. Altium / OrCAD have no bridge yet — the `.lbr`
//! is built, but we invite the user to build the bridge and contribute it.
use serde_json::{json, Value};
use std::path::Path;
use std::process::Command;
fn ad(verb: &str, payload: Value) -> Result<Value, String> {
let mut cmd = Command::new("adom-desktop");
// When the relay has >1 client (e.g. laptop + a cloud VM), a bare verb is
// ambiguous. Honor ADOM_DESKTOP_TARGET to pin the host; single-client setups
// need nothing.
if let Ok(t) = std::env::var("ADOM_DESKTOP_TARGET") {
if !t.is_empty() {
cmd.args(["--target", &t]);
}
}
let out = cmd
.args([verb, &payload.to_string()])
.output()
.map_err(|e| format!("adom-desktop unreachable: {e}"))?;
let txt = String::from_utf8_lossy(&out.stdout).to_string();
Ok(serde_json::from_str::<Value>(&txt).unwrap_or_else(|_| json!({ "ok": out.status.success(), "raw": txt })))
}
fn looks_offline(v: &Value) -> bool {
let s = v.to_string().to_lowercase();
v.get("bridgeConnected").and_then(|b| b.as_bool()) == Some(false)
|| s.contains("not connected") || s.contains("no client")
|| s.contains("offline") || s.contains("timeout")
}
/// Altium / OrCAD: we generate a real EAGLE `.lbr` and hand it over as a
/// downloadable file with tool-accurate import guidance. Altium imports EAGLE
/// libraries natively (its Import Wizard); OrCAD has no native EAGLE path, so we
/// say so honestly. Either way a desktop *auto-import bridge* would streamline
/// it — that's the contribution CTA.
fn library_export(tool: &str, mpn: &str, lbr_name: &str) -> Value {
let asset_url = format!("/asset/{mpn}/{lbr_name}");
let (works, import_hint) = if tool == "Altium" {
(true, "In Altium: File → Import Wizard → \"EAGLE Projects and Designs\" → select this .lbr → it generates the .SchLib / .PcbLib / .IntLib.")
} else {
(false, "OrCAD Capture can't import EAGLE .lbr directly (it uses .olb). For now route it through an EAGLE→OrCAD translator or EDIF; a native OrCAD (.olb) exporter is on the roadmap.")
};
json!({
"ok": false,
"needsBridge": true,
"target": tool,
"assetUrl": asset_url,
"importHint": import_hint,
"exportWorks": works,
"contribute": "https://wiki.adom.inc/adom/adom-desktop",
"message": format!(
"Built a real EAGLE .lbr for {tool} (symbol + footprint, lint-passed). {}",
if works {
"Download it below and import it — Altium reads EAGLE libraries natively. A one-click Adom Desktop bridge would auto-import it; want to build that? Contribute on the wiki."
} else {
"Download it below. A native OrCAD exporter (and an Adom Desktop bridge to auto-import) aren't built yet — want OrCAD in the Adom ecosystem? Contribute on the wiki."
}
)
})
}
/// Deliver chip `mpn` (files in `dir`) to `target`.
pub fn deliver(mpn: &str, dir: &Path, target: &str) -> Value {
let sym = dir.join(format!("{mpn}.kicad_sym"));
let md = dir.join(format!("{mpn}.kicad_mod"));
if !sym.is_file() && !md.is_file() {
return json!({ "ok": false, "error": format!("no KiCad files for {mpn}") });
}
if target == "kicad" {
// Real bridge verbs (verified), not the old send_files theater: the symbol
// installs into the Adom library AND opens on the Symbol Editor canvas; the
// footprint installs into the Adom footprint library (deterministic).
let mut results: Vec<(&str, Result<Value, String>)> = Vec::new();
if let Ok(content) = std::fs::read(&sym) {
results.push(("symbol", ad("kicad_install_symbol", json!({
"fileName": format!("{mpn}.kicad_sym"),
"fileContent": b64(&content),
"symbolName": mpn,
"openEditor": true,
"timeoutSeconds": 60,
}))));
}
if let Ok(content) = std::fs::read(&md) {
results.push(("footprint", ad("kicad_install_footprint", json!({
"fileName": format!("{mpn}.kicad_mod"),
"fileContent": b64(&content),
"footprintName": mpn,
"openEditor": false,
"timeoutSeconds": 60,
}))));
}
return summarize_kicad_chip(mpn, dir, results);
}
let (tool, app, folder) = match target {
"fusion360" | "fusion" => ("Fusion 360", "fusion360", "fusion"),
"altium" => ("Altium", "altium", "altium"),
"orcad" => ("OrCAD", "orcad", "orcad"),
_ => return json!({ "ok": false, "error": format!("unknown target {target}") }),
};
// Build the .lbr (symbol + footprint) via adom-lbr. For Altium/OrCAD write it
// into the chip's library folder so it's downloadable via /asset/<mpn>/.
let lbr_name = if app == "altium" || app == "orcad" { format!("{mpn}.lbr") } else { format!("{mpn}.send.lbr") };
let lbr_path = dir.join(&lbr_name);
let mut g = Command::new("adom-lbr");
g.arg("generate");
if sym.is_file() { g.args(["--sym"]).arg(&sym); }
if md.is_file() { g.args(["--fp"]).arg(&md); }
g.args(["--name", mpn]).arg("--output").arg(&lbr_path);
// adom-lbr v1.6.0 BOM contract: pass the supply-chain IDs we already looked
// up (MF / MPN / Mouser / DigiKey / LCSC) so the library carries them for
// the Fusion/Altium/OrCAD BOM. Omitted distributors are noted, not fatal.
g.args(crate::bom::flags(&dir, mpn));
match g.output() {
Ok(o) if o.status.success() || lbr_path.is_file() => {}
Ok(o) => return json!({ "ok": false, "error": "adom-lbr generate failed", "detail": String::from_utf8_lossy(&o.stderr) }),
Err(e) => return json!({ "ok": false, "error": format!("adom-lbr not available: {e}") }),
}
if app == "altium" || app == "orcad" {
return library_export(tool, mpn, &lbr_name);
}
let send = ad("send_files", json!({ "filePaths": [lbr_path.to_string_lossy()], "targetApp": app, "destinationFolder": folder }));
let win = send.as_ref().ok()
.and_then(|s| s.get("destinationPaths").and_then(|p| p.as_array()).and_then(|a| a.first()).and_then(|x| x.as_str()).map(String::from))
.unwrap_or_else(|| lbr_path.to_string_lossy().to_string());
let mut steps = vec![send];
if app == "fusion360" {
steps.push(ad("fusion_open_lbr", json!({ "filePath": win })));
}
summarize(tool, "library (.lbr)", &steps)
}
fn summarize(tool: &str, what: &str, results: &[Result<Value, String>]) -> Value {
for r in results {
match r {
Err(e) => return json!({ "ok": false, "target": tool, "error": e, "offline": e.contains("unreachable") }),
Ok(v) if looks_offline(v) => {
return json!({ "ok": false, "target": tool, "offline": true,
"message": format!("File generated, but Adom Desktop isn't connected to {tool}. Open Adom Desktop on your laptop and try again.") });
}
_ => {}
}
}
json!({ "ok": true, "target": tool, "message": format!("Delivered {what} to {tool}.") })
}
/// Standard base64 (no external crate) — for sending file content through the bridge.
fn b64(data: &[u8]) -> String {
const T: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity((data.len() + 2) / 3 * 4);
for chunk in data.chunks(3) {
let b0 = chunk[0];
let b1 = *chunk.get(1).unwrap_or(&0);
let b2 = *chunk.get(2).unwrap_or(&0);
out.push(T[(b0 >> 2) as usize] as char);
out.push(T[(((b0 & 0x03) << 4) | (b1 >> 4)) as usize] as char);
out.push(if chunk.len() > 1 { T[(((b1 & 0x0f) << 2) | (b2 >> 6)) as usize] as char } else { '=' });
out.push(if chunk.len() > 2 { T[(b2 & 0x3f) as usize] as char } else { '=' });
}
out
}
/// True if a single bridge response confirms real success (the bridge's `success`
/// flag, or its confirmation text — handles AD wrapping the result in `output`).
fn bridge_ok(r: &Result<Value, String>) -> bool {
match r {
Err(_) => false,
Ok(v) => {
let s = v.to_string().to_lowercase();
v.get("success").and_then(|b| b.as_bool()) == Some(true)
|| s.contains("loaded in the symbol editor")
|| (s.contains("added to") && s.contains("library"))
|| (s.contains("installed") && s.contains("library"))
}
}
}
/// A descriptive paste-into-Claude prompt — always offered so the user can finish
/// the delivery in their own AI thread if anything is off.
fn claude_prompt_chip(mpn: &str, dir: &Path) -> String {
format!(
"I fetched the KiCad CAD for chip \"{mpn}\" (files in `{}`: {mpn}.kicad_sym and {mpn}.kicad_mod). Please \
install them into my KiCad \"Adom\" symbol + footprint libraries and open the symbol in the KiCad Symbol \
Editor so I can see it — use the adom-desktop KiCad bridge (`kicad_install_symbol` with openEditor:true, \
and `kicad_install_footprint`), then confirm they actually landed.",
dir.display()
)
}
/// Summarize the chip's KiCad delivery across symbol + footprint. Verifies REAL
/// per-artifact success and always includes a paste-into-Claude prompt fallback.
fn summarize_kicad_chip(mpn: &str, dir: &Path, results: Vec<(&str, Result<Value, String>)>) -> Value {
let prompt = claude_prompt_chip(mpn, dir);
// Offline / unreachable short-circuit (clear, actionable).
for (_what, r) in &results {
let offline = match r {
Err(e) => e.contains("unreachable"),
Ok(v) => looks_offline(v),
};
if offline {
return json!({ "ok": false, "target": "KiCad", "offline": true, "claudePrompt": prompt,
"message": "Adom Desktop isn't connected. Open it on your laptop and retry — or paste the prompt into Claude." });
}
}
let delivered: Vec<&str> = results.iter().filter(|(_, r)| bridge_ok(r)).map(|(w, _)| *w).collect();
let failed: Vec<&str> = results.iter().filter(|(_, r)| !bridge_ok(r)).map(|(w, _)| *w).collect();
if failed.is_empty() && !delivered.is_empty() {
json!({ "ok": true, "target": "KiCad", "claudePrompt": prompt, "delivered": delivered,
"message": format!("Delivered {} for {mpn} to KiCad (symbol opened in the Symbol Editor).", delivered.join(" + ")) })
} else {
json!({ "ok": false, "target": "KiCad", "claudePrompt": prompt,
"delivered": delivered, "failed": failed,
"message": format!("KiCad delivery didn't fully confirm (ok: {}; unconfirmed: {}). Paste the prompt into Claude to finish it.",
if delivered.is_empty() { "none".into() } else { delivered.join("+") },
if failed.is_empty() { "none".into() } else { failed.join("+") }) })
}
}