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.
← All Pull Requests
kicad-lane: 1.0.92, sch_batch and log_tail in the standard's shapes #54
Merged opened by Colby Knox 2026-10-30
1.0.92: the standard's sch_batch and log_tail shapes (#29).
- kicad_sch_batch: ops can be named by the standard's schematic verbs (sch_place_part, sch_connect, sch_edit, sch_swap_part, sch_text, sch_junction, ...) as well as KiCad's own (place_symbol, wire, ...), mixed in one batch.
- sch_connect and sch_edit run as the KiCad verb their arguments call for (wire, label, connect_to_net, no_connect; move, rotate, delete, set_property).
- sch_place_part, sch_connect, sch_edit and sch_swap_part take KiCad's arguments.
- The reply names ercBefore and ercAfter. Any op that fails, an unknown one included, is named by failedOp, and nothing is written.
- Checked: sch_place_part, sch_text, place_symbol and sch_edit ran in one write on a copy of the inverting amplifier sheet.
- kicad_log_tail: answers the standard's entries [{level, text}] and next beside lines and nextOffset.
Battery 335/0/1.
🤖 Generated with Claude Code
Diff Skip to comments
@@ -1,1307 +1,1308 @@⋯ 318 unchanged lines ⋯ [[package]] name = "kicad-bridge"-version = "1.0.91"+version = "1.0.92" dependencies = [ "adom-eda-checks", "kicad-core",⋯ 5 unchanged lines ⋯ [[package]] name = "kicad-core"-version = "1.0.91"+version = "1.0.92" dependencies = [ "kicad-ipc-rs", "kicad-platform",⋯ 20 unchanged lines ⋯ [[package]] name = "kicad-platform"-version = "1.0.91"+version = "1.0.92" dependencies = [ "serde", "serde_json",⋯ 940 unchanged lines ⋯ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"+@@ -1,27 +1,28 @@⋯ 2 unchanged lines ⋯ members = ["crates/*"] [workspace.package]-version = "1.0.91"+version = "1.0.92" edition = "2021" license = "MIT" publish = false⋯ 15 unchanged lines ⋯ codegen-units = 1 strip = true panic = "abort"+@@ -1,1895 +1,1901 @@⋯ 75 unchanged lines ⋯ # 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,⋯ 66 unchanged lines ⋯ "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"),⋯ 1060 unchanged lines ⋯ 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"})⋯ 670 unchanged lines ⋯ if __name__ == "__main__": sys.exit(main())+@@ -1,532 +1,539 @@⋯ 239 unchanged lines ⋯ 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 } ⋯ 284 unchanged lines ⋯ _ => format!("ERC did not run ({}); the file was not validated.", after.reason.clone().or_else(|| before.reason.clone()).unwrap_or_default()), } }+@@ -1,1356 +1,1421 @@⋯ 1067 unchanged lines ⋯ // 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) {⋯ 235 unchanged lines ⋯ } #[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"); let lib = norm(&fixtures().join("MiniLib.kicad_sym"));⋯ 29 unchanged lines ⋯ err("kicad_sch_batch", &p, json!({}), "missing_arg"); } }+@@ -1,452 +1,455 @@⋯ 134 unchanged lines ⋯ 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,⋯ 36 unchanged lines ⋯ "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.", })⋯ 266 unchanged lines ⋯ assert_eq!(r["windowSource"], json!("test")); } }+
Comments
Log in to comment.
No comments yet.