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
1.0.4: 1.0.4: kicad_add_zone (copper pours as one Undo step with a filled DRC preflight) and kicad_zone_state (filled copper per layer, the ablation metric)
6 files changed
+142−5
rust/Cargo.lock+4−3@@ -244,7 +244,7 @@ dependencies = [ [[package]] name = "kicad-bridge"-version = "1.0.3"+version = "1.0.4" dependencies = [ "kicad-core", "kicad-platform",@@ -255,10 +255,11 @@ dependencies = [ [[package]] name = "kicad-core"-version = "1.0.3"+version = "1.0.4" dependencies = [ "kicad-ipc-rs", "kicad-platform",+ "prost-types", "quick-xml", "serde", "serde_json",@@ -280,7 +281,7 @@ dependencies = [ [[package]] name = "kicad-platform"-version = "1.0.3"+version = "1.0.4" dependencies = [ "serde", "serde_json",
rust/Cargo.toml+1−1@@ -3,7 +3,7 @@ resolver = "2" members = ["crates/*"] [workspace.package]-version = "1.0.3"+version = "1.0.4" edition = "2021" license = "MIT" publish = false
rust/crates/kicad-bridge/src/verbs_routing.rs+26@@ -47,6 +47,26 @@ pub static VERBS: &[Verb] = &[ related: &["kicad_route_net", "kicad_routing_state"], pitfalls: &["Does not save the file or delete a whole net implicitly."], },+ Verb {+ name: "kicad_add_zone",+ summary: "Add copper zones (pours) live as one Undo step: DRC preflight with the zone filled, then created and filled through the IPC API.",+ mechanism: Mechanism::Ipc, risk: "write", timeout_sec: 130,+ input: "{\"filePath\", \"expectedRevision\", \"net\", \"layers\": [\"F.Cu\"], \"polygon\": [[x, y], ...] mm, \"priority\"?: 0, \"clearance\"?: 0.2, \"minThickness\"?: 0.25, \"connection\"?: \"thermal\"|\"solid\", \"thermalGap\"?, \"thermalBridge\"?, \"name\"?, \"dryRun\"?, \"validate\"?, \"refill\"?, \"save\"?} or {\"zones\": [ {...}, ... ]}",+ example: "kicad_add_zone {\"filePath\":\"C:/p/b.kicad_pcb\",\"expectedRevision\":\"<rev>\",\"net\":\"+VBAT\",\"layers\":[\"F.Cu\",\"B.Cu\"],\"polygon\":[[130,60],[166,60],[166,90],[130,90]],\"priority\":2}",+ hint: "Pours for current, heat and copper retention (ablation): power and return nets first, higher priority for the smaller nets inside a bigger pour, thermal reliefs by default (solid for the power stage). The preflight fills the zone on a snapshot and refuses new DRC errors; kicad_zone_state reads the filled copper back.",+ related: &["kicad_zone_state", "kicad_route_net", "kicad_routing_validate", "kicad_routing_state"],+ pitfalls: &["A zone is copper on every listed layer inside the polygon that clears other nets; it does not connect two islands of its net by itself: check kicad_routing_validate for unconnected items after the fill.", "Do not pour switching nodes or sensitive analog nets wide; see the kicad-copper-pours skill for which nets get a pour and which do not.", "Coordinates are mm, +y down, board frame."],+ },+ Verb {+ name: "kicad_zone_state",+ summary: "Read every zone on the live board with KiCad's filled copper area per layer and the coverage.",+ mechanism: Mechanism::Ipc, risk: "read", timeout_sec: 130,+ input: "{\"filePath\": \"C:/.../x.kicad_pcb\", \"refill\"?: true}",+ example: "kicad_zone_state {\"filePath\":\"C:/p/b.kicad_pcb\",\"refill\":true}",+ hint: "refill:true refills every zone first (fills go stale after routing). filledAreaByLayerMm2 is what an ablation process leaves on the board.",+ related: &["kicad_add_zone", "kicad_routing_validate"],+ pitfalls: &["Coverage is against the outline's bounding box; a non-rectangular board reads low.", "Pads, tracks and vias outside zones are not counted."],+ }, Verb { name: "kicad_routing_validate", summary: "Run KiCad DRC against the current unsaved live board snapshot.",@@ -125,6 +145,8 @@ pub fn dispatch(state: &mut State, command: &str, args: &Value) -> Option<Value> "kicad_route_net" => live(state, args, Live::RouteNet), "kicad_remove_route" => live(state, args, Live::RemoveRoute), "kicad_routing_validate" => live(state, args, Live::Validate),+ "kicad_add_zone" => live(state, args, Live::AddZone),+ "kicad_zone_state" => live(state, args, Live::ZoneState), "kicad_board_pads" => board_pads(args), "kicad_add_track" => add_track(args), "kicad_add_via" => add_via(args),@@ -145,6 +167,8 @@ enum Live { RouteNet, RemoveRoute, Validate,+ AddZone,+ ZoneState, } #[cfg(feature = "ipc")]@@ -160,6 +184,8 @@ fn live(state: &mut State, args: &Value, which: Live) -> Value { Live::RouteNet => ipc::route_net(&ctx, args), Live::RemoveRoute => ipc::remove_route(&ctx, args), Live::Validate => ipc::routing_validate(&ctx, args),+ Live::AddZone => ipc::add_zone(&ctx, args),+ Live::ZoneState => ipc::zone_state(&ctx, args), }; match result { Ok(v) => v,
rust/crates/kicad-core/Cargo.toml+2−1@@ -16,6 +16,7 @@ ureq = { version = "3", default-features = false, features = ["rustls", "gzip"], # KiCad IPC API client (protobuf over nng). Behind the ipc feature: nng-sys is a C build # (cmake), so a bare cross type-check can turn it off. kicad-ipc-rs = { version = "0.5", features = ["blocking"], optional = true }+prost-types = { version = "0.14", optional = true } [features] # net: HTTPS fetches (wiki design rules, PCM, KiCad update check) through ureq + rustls.@@ -23,4 +24,4 @@ kicad-ipc-rs = { version = "0.5", features = ["blocking"], optional = true } # from a box without one runs with --no-default-features. default = ["net", "ipc"] net = ["dep:ureq"]-ipc = ["dep:kicad-ipc-rs"]+ipc = ["dep:kicad-ipc-rs", "dep:prost-types"]
rust/crates/kicad-core/src/ipc.rs+108@@ -28,6 +28,7 @@ use kicad_ipc_rs::{BoardLayerInfo, BoardNet, DocumentSpecifier, DocumentType, Ed use crate::cli; use crate::pcb::{self, Board, Plan, RoutingError}; use crate::placement;+use crate::zones; /// The ipc cargo feature is compiled in. The verb group asks this before promising live /// routing; a build with the feature off has no `ipc` module at all.@@ -767,6 +768,113 @@ pub fn routing_validate(ctx: &Ctx, args: &Value) -> RResult<Value> { Ok(out) } +// ---------------------------------------------------------------------------+// Zones: kicad_add_zone, kicad_zone_state+// ---------------------------------------------------------------------------++/// `kicad_add_zone`: one or more copper zones (pours) as ONE native Undo step, with the+/// same revision guard and DRC preflight as `route_net`: the zones' s-expressions are+/// spliced into the editor's snapshot, KiCad's DRC (zones refilled) must add no error, or+/// nothing lands. After the commit the new zones are filled (`refill:false` skips that).+pub fn add_zone(ctx: &Ctx, args: &Value) -> RResult<Value> {+ let s = connect(ctx, args)?;+ let (text, data, revision) = s.snapshot()?;+ check_revision(args, &revision)?;+ let specs = zones::parse_specs(&data, args)?;+ let blocks: Vec<String> = specs.iter().map(zones::zone_sexpr).collect();+ let mut check = Value::Null;+ if args.get("validate") != Some(&json!(false)) {+ let source = s.source_path();+ let before = drc_snapshot(ctx.kicad_cli.as_deref(), &source, &text)?;+ let after = drc_snapshot(ctx.kicad_cli.as_deref(), &source, &pcb::splice(&text, &blocks))?;+ let added = new_errors(&before, &after)?;+ if !added.is_empty() {+ return Err(RoutingError::new("drc_rejected", "The zone adds DRC errors once filled; no live copper was changed").with("violations", json!(added)).with("mutated", json!(false)));+ }+ check = after;+ }+ let public: Vec<Value> = specs.iter().map(zones::ZoneSpec::public).collect();+ if args.get("dryRun") == Some(&json!(true)) {+ return Ok(json!({"success": true, "dryRun": true, "revision": revision, "drc": check, "zones": public,+ "_hint": "Zones validated (filled and DRC-checked on a snapshot) without editing. Submit with dryRun:false and the same revision."}));+ }+ check_revision(args, &s.revision()?)?;+ let nets = s.client.get_nets().map_err(map_err)?;+ let mut items = Vec::with_capacity(specs.len());+ for z in &specs {+ let Some(net) = nets.iter().find(|n| n.name == z.net.name) else {+ return Err(RoutingError::new("unknown_net", format!("The live board has no net named '{}'", z.net.name)).with("mutated", json!(false)));+ };+ let mut ids = Vec::with_capacity(z.layers.len());+ for l in &z.layers {+ ids.push(BoardLayerInfo::id_from_name(l).ok_or_else(|| RoutingError::new("unknown_layer", format!("KiCad has no layer named {l}")))?);+ }+ items.push(prost_types::Any { type_url: zones::ZONE_TYPE_URL.to_string(), value: zones::zone_bytes(z, &ids, net.code) });+ }+ let count = items.len();+ let label = format!("Adom zone {}", specs.iter().map(|z| z.net.name.clone()).collect::<Vec<_>>().join(", "));+ let created = transaction(&s, &label, || {+ let created = s.client.create_items(items, None)?;+ if created.len() != count {+ return Err(IpcFailure::from("KiCad did not create every requested zone".to_string()));+ }+ Ok(created)+ })?;+ let ids: Vec<String> = created.iter().filter_map(|a| zones::zone_id(&a.value)).collect();+ let mut result = json!({+ "success": true, "mutated": true, "source": "live-editor",+ "itemIds": ids, "undoSteps": 1, "zones": public, "drc": check, "saved": false, "refilled": false,+ "_hint": "Zones committed live as one KiCad Undo step and filled. kicad_zone_state reads the filled copper per layer; kicad_routing_validate is the DRC on the live board. The file is saved only with save:true.",+ });+ if ids.len() != count {+ result["postCommitError"] = json!(format!("KiCad answered with {} zone ids for {} zones", ids.len(), count));+ }+ if args.get("refill") != Some(&json!(false)) && !ids.is_empty() {+ match s.client.refill_zones(ids.clone()) {+ Ok(()) => result["refilled"] = json!(true),+ Err(e) => result["postCommitError"] = json!(format!("zones created but not filled: {e}")),+ }+ }+ match s.revision() {+ Ok(r) => {+ result["revision"] = json!(r);+ if args.get("save") == Some(&json!(true)) {+ match s.client.save_document() {+ Ok(()) => result["saved"] = json!(true),+ Err(e) => result["postCommitError"] = json!(e.to_string()),+ }+ }+ }+ Err(e) => result["postCommitError"] = json!(e.message),+ }+ Ok(result)+}++/// `kicad_zone_state`: every zone on the live board with its filled copper area per layer+/// (KiCad's own fill polygons), the sum per layer and the coverage against the outline+/// box. `refill:true` refills every zone first (the fill can be stale after routing).+pub fn zone_state(ctx: &Ctx, args: &Value) -> RResult<Value> {+ let s = connect(ctx, args)?;+ let mut refilled = false;+ if args.get("refill") == Some(&json!(true)) {+ s.client.refill_zones(Vec::new()).map_err(map_err)?;+ refilled = true;+ }+ let (_text, data, revision) = s.snapshot()?;+ let raws = s.client.get_items_raw_by_type_codes(vec![PcbObjectTypeCode::new_zone().code]).map_err(map_err)?;+ let mut read = Vec::with_capacity(raws.len());+ for a in &raws {+ read.push(zones::decode_zone(&a.value).map_err(|e| RoutingError::new("zone_undecodable", format!("KiCad returned a zone the bridge could not decode: {e}")))?);+ }+ let mut out = json!({+ "success": true, "boardPath": s.file_path, "revision": revision, "source": "live-editor",+ "kicadVersion": s.version.full_version, "socket": s.socket, "refilled": refilled,+ "_hint": "filledAreaByLayerMm2 is the copper KiCad's zones keep on each layer: for a copper-ablation process, what does not have to be milled away. Add pours with kicad_add_zone; kicad_routing_validate is the DRC.",+ });+ merge(&mut out, &zones::state_json(&read, &data));+ Ok(out)+}+ // --------------------------------------------------------------------------- // Placement: kicad_placement_state, kicad_move_footprint, kicad_placement_validate // ---------------------------------------------------------------------------
rust/crates/kicad-core/src/lib.rs+1@@ -16,6 +16,7 @@ pub mod bridge_log; pub mod install; pub mod pcb; pub mod placement;+pub mod zones; #[cfg(feature = "ipc")] pub mod ipc; pub mod uninstall;