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.
← Commit history
Merge PR #361: kicad-lane: 1.0.92, sch_batch and log_tail in the standard's shapes
6 files changed
+88−11
rust/Cargo.lock+3−3@@ -319,7 +319,7 @@ dependencies = [ [[package]] name = "kicad-bridge"-version = "1.0.91"+version = "1.0.92" dependencies = [ "adom-eda-checks", "kicad-core",@@ -331,7 +331,7 @@ dependencies = [ [[package]] name = "kicad-core"-version = "1.0.91"+version = "1.0.92" dependencies = [ "kicad-ipc-rs", "kicad-platform",@@ -358,7 +358,7 @@ dependencies = [ [[package]] name = "kicad-platform"-version = "1.0.91"+version = "1.0.92" dependencies = [ "serde", "serde_json",
rust/Cargo.toml+1−1@@ -3,7 +3,7 @@ resolver = "2" members = ["crates/*"] [workspace.package]-version = "1.0.91"+version = "1.0.92" edition = "2021" license = "MIT" publish = false
rust/crates/kicad-bridge/src/verbs_sch_edit.rs+67−3@@ -1068,16 +1068,67 @@ fn connect_to_bus(ctx: &mut Ctx) -> Result<Done, Value> { // Batch // --------------------------------------------------------------------------- +/// The batch's ops in KiCad's own names and shapes: an op may name the standard's schematic verb (sch_place_part,+/// sch_connect, sch_edit, ... with the standard's arguments, the standard's sch_batch #29) and is turned into KiCad's+/// the way a direct call is (catalog alias, then std_shapes); one standard op can become several KiCad ops.+fn native_ops(ops: &[Value]) -> Result<Vec<Value>, Value> {+ let mut out = Vec::new();+ for (i, opv) in ops.iter().enumerate() {+ let name = opv.get("op").and_then(Value::as_str).map(str::trim).ok_or_else(|| fail("bad_arg", format!("ops[{i}] has no op"), "Every op names its operation: {\"op\": \"sch_place_part\", ...}."))?;+ // the four standard schematic verbs KiCad answers under its own names (equivalents/kicad.json): the op runs as+ // KiCad's verb, chosen by the arguments where one standard verb is several KiCad ones, with KiCad's arguments+ let has = |k: &str| opv.get(k).is_some_and(|v| !v.is_null());+ let mapped = match name.trim_start_matches("kicad_") {+ "sch_place_part" => Some("sch_place_symbol"),+ "sch_swap_part" => Some("sch_replace_symbol"),+ "sch_connect" if has("net") && (has("pin") || has("pins") || has("reference")) => Some("sch_connect_to_net"),+ "sch_connect" if has("points") || has("from") => Some("sch_wire"),+ "sch_connect" if has("text") || has("label") => Some("sch_label"),+ "sch_connect" => Some("sch_no_connect"),+ "sch_edit" if opv.get("delete") == Some(&json!(true)) => Some("sch_delete"),+ "sch_edit" if has("rotation") || has("angle") => Some("sch_rotate"),+ "sch_edit" if has("to") || has("by") || has("at") => Some("sch_move"),+ "sch_edit" => Some("sch_set_property"),+ _ => None,+ };+ let name = mapped.unwrap_or(name);+ let called = if name.starts_with("kicad_") { name.to_string() } else { format!("kicad_{name}") };+ let full = crate::catalog::alias_target(&called).map(String::from).unwrap_or_else(|| called.clone());+ let mut args = opv.clone();+ if let Some(o) = args.as_object_mut() {+ o.remove("op");+ }+ match crate::std_shapes::to_native(&called, &full, &args) {+ Some(Err(refusal)) => return Err(refusal),+ Some(Ok(sh)) if sh.command == "kicad_sch_batch" => out.extend(sh.args["ops"].as_array().cloned().unwrap_or_default()),+ Some(Ok(sh)) => {+ let mut a = sh.args.clone();+ a["op"] = json!(sh.command.trim_start_matches("kicad_sch_").trim_start_matches("kicad_"));+ out.push(a);+ }+ None => {+ let mut a = opv.clone();+ a["op"] = json!(name);+ out.push(a);+ }+ }+ }+ Ok(out)+}+ fn batch(ctx: &mut Ctx) -> Result<Done, Value> {- let ops = ctx.args.get("ops").and_then(Value::as_array).filter(|a| !a.is_empty()).ok_or_else(|| fail("missing_arg", "ops is required", "Pass ops as a list of {\"op\": \"wire\", ...that verb's arguments}."))?.clone();+ let given = ctx.args.get("ops").and_then(Value::as_array).filter(|a| !a.is_empty()).ok_or_else(|| fail("missing_arg", "ops is required", "Pass ops as a list of {\"op\": \"sch_place_part\" | \"place_symbol\", ...that verb's arguments}."))?.clone();+ let ops = native_ops(&given)?; let mut results = Vec::new(); for (i, opv) in ops.iter().enumerate() {- let name = opv.get("op").and_then(Value::as_str).map(|s| s.trim().trim_start_matches("kicad_sch_").to_string()).ok_or_else(|| fail("bad_arg", format!("ops[{i}] has no op"), "Every op names its operation: {\"op\": \"wire\", ...}."))?;+ let name = opv.get("op").and_then(Value::as_str).map(|s| s.trim().trim_start_matches("kicad_").trim_start_matches("sch_").to_string()).ok_or_else(|| fail("bad_arg", format!("ops[{i}] has no op"), "Every op names its operation: {\"op\": \"wire\", ...}."))?; if name == "batch" { return Err(fail("bad_arg", format!("ops[{i}] is a batch; batches do not nest"), "Flatten the ops into one list.")); } let Some(f) = op_for(&name) else {- return Err(fail("bad_arg", format!("ops[{i}]: unknown op {name}"), "Ops: place_symbol, wire, label, move, rotate, delete, set_property, no_connect, move_region, annotate, replace_symbol, update_from_library, reset_fields, delete_wire, split_wire, edit_label, junction, connect_to_net, connect_pins, text, bus, bus_entry, connect_to_bus."));+ let mut v = fail("bad_arg", format!("ops[{i}]: unknown op {name}"), "Ops: the standard's schematic verbs (sch_place_part, sch_connect, sch_edit, ...) or KiCad's: place_symbol, wire, label, move, rotate, delete, set_property, no_connect, move_region, annotate, replace_symbol, update_from_library, reset_fields, delete_wire, split_wire, edit_label, junction, connect_to_net, connect_pins, text, bus, bus_entry, connect_to_bus.");+ v["failedOp"] = json!(i);+ return Err(v); }; let mut sub = Ctx { sch: &mut *ctx.sch, path: ctx.path, lib: ctx.lib, args: opv }; match f(&mut sub) {@@ -1318,6 +1369,19 @@ mod tests { err("kicad_sch_connect_to_bus", &p, json!({"pins": ["U1.6"], "bus": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "members": ["X"], "entry": "up"}), "entry_off_bus"); } + #[test]+ fn batch_takes_the_standards_op_names() {+ let p = temp_sheet("stdbatch");+ let v = call("kicad_sch_batch", &p, json!({"ops": [+ {"op": "sch_junction", "at": [101.6, 101.6]},+ {"op": "sch_place_part", "libId": "Device:R", "reference": "R77", "at": [127.0, 127.0]},+ {"op": "sch_text", "text": "standard names", "at": [50.8, 50.8]}]}));+ assert_eq!(v["success"], true, "{v}");+ assert_eq!(v["results"].as_array().map(|a| a.len()), Some(3), "{v}");+ let bad = call("kicad_sch_batch", &p, json!({"ops": [{"op": "sch_text", "text": "x", "at": [1, 1]}, {"op": "sch_no_such_verb"}]}));+ assert_eq!(bad["failedOp"], 1, "{bad}");+ }+ #[test] fn batch_is_all_or_nothing() { let p = temp_sheet("batch");
rust/crates/kicad-bridge/src/verbs_schematic.rs+6@@ -240,6 +240,12 @@ pub(crate) fn edit(state: &mut State, args: &Value, op: Op) -> Value { o.insert(k.clone(), val.clone()); } }+ // the standard's sch_batch (#29) names the two ERC counts on their own+ if v.get("ops").is_some() && v.get("results").is_some() {+ v["ercBefore"] = v["erc"]["before"].clone();+ v["ercAfter"] = v["erc"]["after"].clone();+ v["mutated"] = json!(true);+ } v }
rust/crates/kicad-core/src/bridge_log.rs+3−1@@ -135,7 +135,7 @@ pub fn log_tail(dir: Option<&Path>, args: &Value) -> Value { let bridge_log = bridge_log_path().filter(|p| p.is_file()).map(|p| p.to_string_lossy().replace('\\', "/")); let Some(p) = p.filter(|p| p.is_file()) else { return json!({- "success": true, "available": false, "lines": [], "nextOffset": 0,+ "success": true, "available": false, "lines": [], "nextOffset": 0, "entries": [], "next": 0, "logDir": dir.map(|d| d.to_string_lossy().replace('\\', "/")), "bridgeLogPath": bridge_log, "_hint": NO_LOG_HINT,@@ -178,6 +178,8 @@ pub fn log_tail(dir: Option<&Path>, args: &Value) -> Value { "logPath": p.to_string_lossy().replace('\\', "/"), "sinceOffset": since, "nextOffset": size, "truncated": truncated, "lineCount": lines.len(), "levels": counts, "lines": lines,+ // the standard's log_tail (#29): entries and next, the same lines and offset+ "entries": lines, "next": size, "bridgeLogPath": bridge_log, "_hint": "Carry nextOffset into the next call and you get ONLY what happened since: tail, act, tail again, and the second read is your action's consequences. errorsOnly:true filters to error/warning.", })
skills/kicad-bridge-test/run_verb_tests.py+8−3@@ -76,6 +76,8 @@ def ok(r: dict) -> bool: # Checks beyond ok(): named lambdas in CHECKS below. CHECKS = {+ # 1.0.92 (standard log_tail #29): entries and next beside lines and nextOffset+ "log_entries": lambda r: ok(r) and isinstance(r.get("entries"), list) and "next" in r and r.get("next") == r.get("nextOffset"), "ok": ok, "verbs>=47": lambda r: ok(r) and len(r.get("verbs") or []) >= 47, "editors_listed": lambda r: ok(r) and "summary" in r,@@ -148,7 +150,7 @@ def matrix(w: dict) -> dict: "list_footprints": dict(phase=1, args={"library": "Resistor_SMD"}, timeout=90), "list_symbols": dict(phase=1, args={"libraryName": "Device"}, timeout=90, note="the #80 regression: a STOCK library must resolve to KiCad's share dir"),- "log_tail": dict(phase=1, args={}),+ "log_tail": dict(phase=1, args={}, check="log_entries"), "progress": dict(phase=1, args={}), "verb_times": dict(phase=1, args={}), "errors": dict(phase=1, args={}, note="win32 text read of KiCad message boxes; none open is fine"),@@ -1215,8 +1217,11 @@ def main(): r = K("sch_update_from_library", {"filePath": sch}); rec("sch_update_from_library", r, ok(r)) r = K("sch_fix_connectivity", {"filePath": sch, "dryRun": True}); rec("sch_fix_connectivity", r, ok(r) and r.get("mutated") is not True) r = K("sch_check", {"filePath": sch}); rec("sch_check", r, ok(r))- r = K("sch_batch", {"filePath": sch, "ops": [{"op": "text", "text": "batch 1", "at": [30.48, 35.56]}, {"op": "text", "text": "batch 2", "at": [30.48, 40.64]}]})- rec("sch_batch", r, ok(r))+ # 1.0.92 (standard sch_batch #29): KiCad's op names and the standard's together; a failing op is named and nothing is written+ r = K("sch_batch", {"filePath": sch, "ops": [{"op": "text", "text": "batch 1", "at": [30.48, 35.56]}, {"op": "sch_text", "text": "batch 2", "at": [30.48, 40.64]},+ {"op": "sch_junction", "at": [30.48, 45.72]}]})+ rb = K("sch_batch", {"filePath": sch, "ops": [{"op": "sch_text", "text": "never", "at": [30.48, 50.8]}, {"op": "sch_no_such_verb"}]})+ rec("sch_batch", {"ok": r, "bad": rb}, ok(r) and len(r.get("results") or []) == 3 and "ercBefore" in r and rb.get("failedOp") == 1) r = K("sch_sheet", {"filePath": sch, "action": "add", "name": "Child", "file": "child.kicad_sch", "at": [127, 30.48], "size": [25.4, 20.32], "projectPath": pro}) rec("sch_sheet", r, ok(r)) r = K("sch_sheet_pins", {"filePath": sch, "sheet": "Child", "action": "add", "name": "VIN", "shape": "input", "side": "left"})