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
1.0.1: 1.0.1: Freerouting on the user's terms. kicad_freerouting {status|install|uninstall} installs Freerouting's own…
471d9cf
1mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
//! Freerouting end to end on the six-net fixture from `demo/routing/make_fixture.py`
//! (rebuilt here from the blank template, as tests/routing_file.rs does): board -> DSN
//! (`dsn::write_dsn`) -> Freerouting headless (`freerouting::run`) -> SES (`dsn::read_ses`)
//! -> copper applied to a copy of the board (`freerouting::apply_to_file`) -> a real KiCad
//! 10 DRC through `service-kicad` that must report zero errors and zero unconnected items
//! (the three `lib_footprint_issues` warnings are the deliberately uninstalled fixture
//! library). Skipped, loudly, when no Freerouting executable is around: the
//! `ADOM_FREEROUTING_EXE` override, the bridge's own install folder, or the scratchpad
//! bundle unpacked while this engine was built.
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Duration;
use serde_json::Value;
use kicad_core::schematic::new_uuid;
use kicad_core::{dsn, freerouting, pcb};
const SCRATCH_BUNDLE: &str = "/tmp/claude-1001/-home-adom-project/a82462cd-99a1-4a08-b142-383081e6ec8a/scratchpad/fr/freerouting-2.4.1-linux-x64/bin/freerouting";
fn repo_file(rel: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../..").join(rel)
}
fn temp_dir(tag: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("kicad-core-freerouting-{tag}-{}-{}", std::process::id(), new_uuid()));
std::fs::create_dir_all(&d).unwrap();
d
}
fn on_path(name: &str) -> Option<PathBuf> {
let path = std::env::var_os("PATH")?;
std::env::split_paths(&path).map(|d| d.join(name)).find(|p| p.is_file())
}
fn freerouting_exe() -> Option<PathBuf> {
if let Some(p) = std::env::var_os(freerouting::ENV_EXE).map(PathBuf::from).filter(|p| p.is_file()) {
return Some(p);
}
if let Some(p) = freerouting::find_exe(&freerouting::dir()) {
return Some(p);
}
Some(PathBuf::from(SCRATCH_BUNDLE)).filter(|p| p.is_file())
}
/// `demo/routing/make_fixture.py`: J1 and J2 with six pads each on NET_1..NET_6, J3 with
/// one pad on NET_6, an Edge.Cuts rectangle, KiCad 10 dialect.
fn fixture_text() -> String {
let base = std::fs::read_to_string(repo_file("templates/blank-board.kicad_pcb")).expect("blank-board template");
let mut blocks = String::new();
let mut footprint = |r: &str, x: f64, y: f64, pads: Vec<(String, f64, f64, String)>| {
blocks.push_str(&format!("(footprint \"RoutingFixture:{r}\" (layer \"F.Cu\") (uuid \"{}\") (at {x} {y})\n", new_uuid()));
blocks.push_str(&format!("(property \"Reference\" \"{r}\" (at 0 -3) (layer \"F.SilkS\") (effects (font (size 1 1) (thickness 0.15))))\n"));
blocks.push_str("(property \"Value\" \"ROUTING_TEST\" (at 0 -5) (layer \"F.Fab\") (effects (font (size 1 1) (thickness 0.15))))\n(attr through_hole exclude_from_pos_files exclude_from_bom)\n");
for (n, px, py, net) in pads {
blocks.push_str(&format!("(pad \"{n}\" thru_hole circle (at {px} {py}) (size 2 2) (drill 1) (layers \"*.Cu\" \"*.Mask\") (net \"{net}\") (uuid \"{}\"))\n", new_uuid()));
}
blocks.push_str(")\n");
};
let six = || (1..=6).map(|i| (i.to_string(), 0.0, ((i - 1) * 5) as f64, format!("NET_{i}"))).collect::<Vec<_>>();
footprint("J1", 105.0, 85.0, six());
footprint("J2", 145.0, 85.0, six());
footprint("J3", 125.0, 116.0, vec![("1".into(), 0.0, 0.0, "NET_6".into())]);
blocks.push_str(&format!("(gr_rect (start 98 78) (end 152 122) (stroke (width 0.05) (type default)) (fill none) (layer \"Edge.Cuts\") (uuid \"{}\"))\n", new_uuid()));
let idx = base.rfind(')').unwrap();
format!("{}{}\n)\n", &base[..idx], blocks)
}
fn drc(svc: &Path, board: &Path, report: &Path) -> Value {
let out = Command::new(svc).args(["pcb", "drc"]).arg(board).args(["--format", "json", "--out"]).arg(report).output().expect("service-kicad runs");
assert!(report.is_file(), "service-kicad pcb drc wrote no report: {}\n{}", String::from_utf8_lossy(&out.stderr), String::from_utf8_lossy(&out.stdout));
let rep: Value = serde_json::from_str(&std::fs::read_to_string(report).unwrap()).expect("DRC report is JSON");
assert!(rep.get("kicad_version").is_some(), "not a DRC report: {rep}");
rep
}
#[test]
fn freerouting_routes_the_six_net_fixture_to_a_clean_drc() {
let Some(exe) = freerouting_exe() else {
eprintln!("SKIPPED: no Freerouting executable ({} unset, nothing under {}, no scratchpad bundle at {SCRATCH_BUNDLE})", freerouting::ENV_EXE, freerouting::dir().display());
return;
};
let dir = temp_dir("route");
eprintln!("work dir {}", dir.display());
let path = dir.join("live-routing.kicad_pcb");
std::fs::write(&path, fixture_text()).unwrap();
let loaded = pcb::load(&serde_json::json!({"filePath": path.to_string_lossy()}), false).unwrap();
let board = &loaded.board;
assert_eq!(pcb::pads(board).len(), 13);
// ---- DSN
let rules = dsn::rules_for(&path, board);
assert_eq!(rules.source, "kicad-defaults");
let out = dsn::write_dsn(board, &loaded.text, "live-routing", &rules, None).unwrap();
assert_eq!(out.stats.routed_nets.len(), 6);
let dsn_path = dir.join("live-routing.dsn");
std::fs::write(&dsn_path, &out.text).unwrap();
// ---- Freerouting
let version = freerouting::probe_version(&exe).expect("--help answers");
assert!(version.starts_with("2."), "{version}");
let ses_path = dir.join("live-routing.ses");
let mut lines = 0usize;
let run = freerouting::run(&exe, &dsn_path, &ses_path, &dir, 20, Some(2), Duration::from_secs(300), |line| {
lines += 1;
if lines <= 60 {
eprintln!(" fr> {line}");
}
})
.expect("Freerouting starts");
eprintln!("Freerouting {} exit {} timedOut {} passes {} seconds {} ({} log lines)", run.version.as_deref().unwrap_or("?"), run.exit_code, run.timed_out, run.passes_seen, run.seconds, lines);
for l in &run.stdout_tail {
eprintln!(" tail> {l}");
}
assert!(!run.timed_out, "Freerouting hit the deadline");
assert!(ses_path.is_file(), "Freerouting wrote no session file (exit {})", run.exit_code);
assert_eq!(run.version.as_deref(), Some(version.as_str()));
// ---- SES -> copper -> file
let ses_text = std::fs::read_to_string(&ses_path).unwrap();
let session = dsn::read_ses(&ses_text).expect("session parses");
let copper = dsn::copper_for(&session, board, None, &rules);
let mut routed: Vec<&str> = copper.iter().map(|c| c.net.as_str()).collect();
routed.sort();
assert_eq!(routed, ["NET_1", "NET_2", "NET_3", "NET_4", "NET_5", "NET_6"], "every net got copper: {:?}", session.nets);
let (bak, segs, vias) = freerouting::apply_to_file(&loaded, &copper, true).expect("write accepted");
eprintln!("applied {segs} segments and {vias} vias; backup {:?}", bak);
assert!(segs >= 6, "at least one segment per net");
assert!(bak.as_ref().map(|b| Path::new(b).is_file()).unwrap_or(false));
let after = pcb::parse_pcb(&path).unwrap();
assert_eq!(after.segments.len(), segs);
assert_eq!(after.vias.len(), vias);
assert_eq!(pcb::pads(&after), pcb::pads(board), "the splice never touches footprints");
for v in &after.vias {
assert_eq!(v.layers.as_ref().unwrap(), &["F.Cu", "B.Cu"]);
assert!(v.drill.unwrap() < v.size.unwrap());
}
// ---- KiCad's verdict
let Some(svc) = on_path("service-kicad") else {
eprintln!("service-kicad not on PATH; copper applied but not DRC-checked here");
return;
};
let report = drc(&svc, &path, &dir.join("r.json"));
let result = kicad_core::cli::summarize_report(&report);
eprintln!("service-kicad DRC on the Freerouting result: {} error(s), {} warning(s), {} unconnected; kicad {}", result["errors"], result["warnings"], result["unconnected"], report["kicad_version"]);
for v in result["first"].as_array().unwrap() {
eprintln!(" {} {}: {}", v["severity"].as_str().unwrap_or(""), v["type"].as_str().unwrap_or(""), v["description"].as_str().unwrap_or(""));
}
assert_eq!(result["unconnected"], 0, "every net is connected by the router's copper");
assert_eq!(result["errors"], 0, "no DRC errors: {}", result["first"]);
let types: Vec<&str> = result["first"].as_array().unwrap().iter().filter_map(|v| v["type"].as_str()).collect();
assert!(types.iter().all(|t| *t == "lib_footprint_issues"), "only the fixture library warnings expected: {types:?}");
eprintln!("RESULT passes={} seconds={} segments={segs} vias={vias} drcErrors=0 unconnected=0 warnings={}", run.passes_seen, run.seconds, result["warnings"]);
let _ = std::fs::remove_dir_all(&dir);
}
/// The Linux install path without the network: `ADOM_FREEROUTING_ZIP` names a local copy
/// of the release zip; the std-only zip reader unpacks it, the platform layer fixes the
/// modes, and the launcher answers `--help`. Skipped when the variable is unset.
#[test]
fn extracts_a_local_bundle_and_verifies_it() {
let Some(zip) = std::env::var_os("ADOM_FREEROUTING_ZIP").map(PathBuf::from).filter(|p| p.is_file()) else {
eprintln!("SKIPPED: ADOM_FREEROUTING_ZIP unset");
return;
};
let dir = temp_dir("extract");
let t = std::time::Instant::now();
let exe = freerouting::extract_bundle(&zip, &dir).expect("extract");
eprintln!("extracted to {} in {:.1} s", exe.display(), t.elapsed().as_secs_f64());
assert!(exe.is_file());
let v = freerouting::probe_version(&exe).expect("--help answers");
eprintln!("Freerouting {v}");
assert_eq!(v, freerouting::VERSION);
let _ = std::fs::remove_dir_all(&dir);
}