app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Drive your own KiCad from an AI, in the background: open every editor, read the whole board as data (the EDA Bridge Standard board model), stack-up, part heights, netlists, place, route, pour, DRC and exports.
master
John Lauer
rust: workspace with kicad-platform (windows, macos, linux), kicad-core, kicad-bridge; 13 headless verbs; alpha.2
51525a8
2mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
//! kicad-cli runner. Every call is quiet (no console window), has a deadline, and returns
//! stdout, stderr and the parsed JSON report when one was requested.
use std::path::Path;
use std::process::Command;
use std::time::{Duration, Instant};
use serde_json::{json, Value};
/// Never let a console child blip a window onto the user's screen.
pub fn quiet(cmd: &mut Command) {
kicad_platform::native().quiet_command(cmd);
}
pub struct CliResult {
pub exit_code: i32,
pub stdout: String,
pub stderr: String,
pub elapsed_ms: u64,
pub timed_out: bool,
}
pub fn run(cli: &Path, args: &[&str], timeout: Duration) -> CliResult {
let mut cmd = Command::new(cli);
cmd.args(args);
quiet(&mut cmd);
cmd.stdout(std::process::Stdio::piped()).stderr(std::process::Stdio::piped());
let t0 = Instant::now();
let mut child = match cmd.spawn() {
Ok(c) => c,
Err(e) => {
return CliResult { exit_code: -1, stdout: String::new(), stderr: format!("spawn failed: {e}"), elapsed_ms: 0, timed_out: false }
}
};
// Poll with a deadline; kill on timeout so a hung kicad-cli never hangs a verb.
loop {
match child.try_wait() {
Ok(Some(_)) => break,
Ok(None) => {
if t0.elapsed() > timeout {
let _ = child.kill();
let _ = child.wait();
return CliResult { exit_code: -2, stdout: String::new(), stderr: "kicad-cli timed out".into(), elapsed_ms: t0.elapsed().as_millis() as u64, timed_out: true };
}
std::thread::sleep(Duration::from_millis(50));
}
Err(e) => {
return CliResult { exit_code: -1, stdout: String::new(), stderr: format!("wait failed: {e}"), elapsed_ms: t0.elapsed().as_millis() as u64, timed_out: false }
}
}
}
let out = child.wait_with_output().unwrap();
CliResult {
exit_code: out.status.code().unwrap_or(-1),
stdout: String::from_utf8_lossy(&out.stdout).to_string(),
stderr: String::from_utf8_lossy(&out.stderr).to_string(),
elapsed_ms: t0.elapsed().as_millis() as u64,
timed_out: false,
}
}
/// Summarise a kicad-cli drc/erc JSON report the way the Python bridge does:
/// counts by severity, unconnected items, and the first violations verbatim.
pub fn summarize_report(report: &Value) -> Value {
let mut errors = 0u64;
let mut warnings = 0u64;
let mut exclusions = 0u64;
let mut first: Vec<Value> = Vec::new();
let mut walk = |arr: &Value| {
if let Some(items) = arr.as_array() {
for v in items {
if v.get("excluded").and_then(Value::as_bool).unwrap_or(false) {
exclusions += 1;
continue;
}
match v.get("severity").and_then(Value::as_str) {
Some("error") => errors += 1,
Some("warning") => warnings += 1,
_ => {}
}
if first.len() < 25 {
first.push(json!({
"type": v.get("type"),
"severity": v.get("severity"),
"description": v.get("description"),
"items": v.get("items").and_then(Value::as_array).map(|a| a.iter().filter_map(|i| i.get("description").cloned()).collect::<Vec<_>>()),
}));
}
}
}
};
walk(report.get("violations").unwrap_or(&Value::Null));
let unconnected = report.get("unconnected_items").and_then(Value::as_array).map(|a| a.len()).unwrap_or(0);
walk(report.get("unconnected_items").unwrap_or(&Value::Null));
walk(report.get("schematic_parity").unwrap_or(&Value::Null));
if let Some(sheets) = report.get("sheets").and_then(Value::as_array) {
for s in sheets {
walk(s.get("violations").unwrap_or(&Value::Null));
}
}
json!({
"errors": errors,
"warnings": warnings,
"excluded": exclusions,
"unconnected": unconnected,
"clean": errors == 0 && unconnected == 0,
"first": first,
"kicadVersion": report.get("kicad_version"),
"source": report.get("source"),
})
}
/// The Python bridge's `data.violations` shape: one entry per violation and per
/// unconnected item, each with its items' description and pos.
pub fn flatten_violations(report: &Value) -> Vec<Value> {
let mut out = Vec::new();
let items_of = |v: &Value| -> Vec<Value> {
v.get("items").and_then(Value::as_array).map(|a| a.iter().map(|i| json!({"description": i.get("description").cloned().unwrap_or(json!("")), "pos": i.get("pos").cloned().unwrap_or(json!({}))})).collect()).unwrap_or_default()
};
let mut push = |arr: Option<&Value>, forced_type: Option<&str>| {
if let Some(items) = arr.and_then(Value::as_array) {
for v in items {
out.push(json!({
"type": forced_type.map(|t| json!(t)).unwrap_or_else(|| v.get("type").cloned().unwrap_or(json!("unknown"))),
"severity": v.get("severity").cloned().unwrap_or(json!(if forced_type.is_some() { "error" } else { "warning" })),
"description": v.get("description").cloned().unwrap_or(json!("")),
"excluded": v.get("excluded").and_then(Value::as_bool).unwrap_or(false),
"items": items_of(v),
}));
}
}
};
push(report.get("violations"), None);
push(report.get("unconnected_items"), Some("unconnected_items"));
push(report.get("schematic_parity"), Some("schematic_parity"));
if let Some(sheets) = report.get("sheets").and_then(Value::as_array) {
for s in sheets {
push(s.get("violations"), None);
}
}
out
}